華為諾亞方舟實(shí)驗(yàn)室常見(jiàn)算法題題目

一、codetest1

4.3約筆試,兩道題,一道考察算法,一道考察深度學(xué)習(xí)框架,兩小時(shí),線下做。

二、codetest2

手寫代碼,檢測(cè)鏈表中是否有環(huán),這個(gè)比較簡(jiǎn)單,和leetcode的一個(gè)題目是一樣的,因?yàn)槲覍懙锰炝耍捅患恿它c(diǎn)難度,如果一個(gè)鏈表中有環(huán),那么判斷哪個(gè)節(jié)點(diǎn)是環(huán)的進(jìn)入節(jié)點(diǎn)。

方法一:哈希表

思路及算法

最容易想到的方法是遍歷所有節(jié)點(diǎn),每次遍歷到一個(gè)節(jié)點(diǎn)時(shí),判斷該節(jié)點(diǎn)此前是否被訪問(wèn)過(guò)。

具體地,我們可以使用哈希表來(lái)存儲(chǔ)所有已經(jīng)訪問(wèn)過(guò)的節(jié)點(diǎn)。每次我們到達(dá)一個(gè)節(jié)點(diǎn),如果該節(jié)點(diǎn)已經(jīng)存在于哈希表中,則說(shuō)明該鏈表是環(huán)形鏈表,否則就將該節(jié)點(diǎn)加入哈希表中。重復(fù)這一過(guò)程,直到我們遍歷完整個(gè)鏈表即可。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head != nullptr) {
            if (seen.count(head)) {
                return true;
            }
            seen.insert(head);
            head = head->next;
        }
        return false;
    }
};
class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        seen = set()
        while head:
            if head in seen:
                return True
            seen.add(head)
            head = head.next
        return False

復(fù)雜度分析

  • 時(shí)間復(fù)雜度:O(N)O(N),其中 NN 是鏈表中的節(jié)點(diǎn)數(shù)。最壞情況下我們需要遍歷每個(gè)節(jié)點(diǎn)一次。

  • 空間復(fù)雜度:O(N)O(N),其中 NN 是鏈表中的節(jié)點(diǎn)數(shù)。主要為哈希表的開銷,最壞情況下我們需要將每個(gè)節(jié)點(diǎn)插入到哈希表中一次。

方法二:快慢指針

思路及算法

本方法需要讀者對(duì)「Floyd 判圈算法」(又稱龜兔賽跑算法)有所了解。

假想「烏龜」和「兔子」在鏈表上移動(dòng),「兔子」跑得快,「烏龜」跑得慢。當(dāng)「烏龜」和「兔子」從鏈表上的同一個(gè)節(jié)點(diǎn)開始移動(dòng)時(shí),如果該鏈表中沒(méi)有環(huán),那么「兔子」將一直處于「烏龜」的前方;如果該鏈表中有環(huán),那么「兔子」會(huì)先于「烏龜」進(jìn)入環(huán),并且一直在環(huán)內(nèi)移動(dòng)。等到「烏龜」進(jìn)入環(huán)時(shí),由于「兔子」的速度快,它一定會(huì)在某個(gè)時(shí)刻與烏龜相遇,即套了「烏龜」若干圈。

我們可以根據(jù)上述思路來(lái)解決本題。具體地,我們定義兩個(gè)指針,一快一滿。慢指針每次只移動(dòng)一步,而快指針每次移動(dòng)兩步。初始時(shí),慢指針在位置 head,而快指針在位置 head.next。這樣一來(lái),如果在移動(dòng)的過(guò)程中,快指針?lè)催^(guò)來(lái)追上慢指針,就說(shuō)明該鏈表為環(huán)形鏈表。否則快指針將到達(dá)鏈表尾部,該鏈表不為環(huán)形鏈表。
細(xì)節(jié)

  • 為什么我們要規(guī)定初始時(shí)慢指針在位置 head,快指針在位置 head.next,而不是兩個(gè)指針都在位置 head(即與「烏龜」和「兔子」中的敘述相同)?
    觀察下面的代碼,我們使用的是 while 循環(huán),循環(huán)條件先于循環(huán)體。由于循環(huán)條件一定是判斷快慢指針是否重合,如果我們將兩個(gè)指針初始都置于 head,那么 while 循環(huán)就不會(huì)執(zhí)行。因此,我們可以假想一個(gè)在 head 之前的虛擬節(jié)點(diǎn),慢指針從虛擬節(jié)點(diǎn)移動(dòng)一步到達(dá) head,快指針從虛擬節(jié)點(diǎn)移動(dòng)兩步到達(dá) head.next,這樣我們就可以使用 while 循環(huán)了。

  • 當(dāng)然,我們也可以使用 do-while 循環(huán)。此時(shí),我們就可以把快慢指針的初始值都置為 head。

class Solution {
public:
    bool hasCycle(ListNode* head) {
        if (head == nullptr || head->next == nullptr) {
            return false;
        }
        ListNode* slow = head;
        ListNode* fast = head->next;
        while (slow != fast) {
            if (fast == nullptr || fast->next == nullptr) {
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }
        return true;
    }
};
class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        if not head or not head.next:
            return False
        
        slow = head
        fast = head.next

        while slow != fast:
            if not fast or not fast.next:
                return False
            slow = slow.next
            fast = fast.next.next
        
        return True

復(fù)雜度分析

  • 時(shí)間復(fù)雜度:O(N)O(N),其中 NN 是鏈表中的節(jié)點(diǎn)數(shù)。
    當(dāng)鏈表中不存在環(huán)時(shí),快指針將先于慢指針到達(dá)鏈表尾部,鏈表中每個(gè)節(jié)點(diǎn)至多被訪問(wèn)兩次。
    當(dāng)鏈表中存在環(huán)時(shí),每一輪移動(dòng)后,快慢指針的距離將減小一。而初始距離為環(huán)的長(zhǎng)度,因此至多移動(dòng) NN 輪。
  • 空間復(fù)雜度:O(1)O(1)。我們只使用了兩個(gè)指針的額外空間。

手寫代碼,判斷一個(gè)無(wú)序數(shù)組中,最長(zhǎng)的連續(xù)相同的元素個(gè)數(shù),這個(gè)也挺簡(jiǎn)單的,就是注意一下跳出的時(shí)候最后一次有沒(méi)有進(jìn)行比較就行了,面試官說(shuō)看你思路清晰,經(jīng)常刷題吧,勉強(qiáng)算你過(guò)了吧。

三、codetest3

題目1:
A為一個(gè)十進(jìn)制數(shù)(以整數(shù)為例),k位,k<100。求B使得B為大于A的最小整數(shù),且A各位的和等于B各位的和。


題目2:
給一定數(shù)量的信封,帶有整數(shù)對(duì) (w, h) 分別代表信封寬度和高度。一個(gè)信封的寬高均大于另一個(gè)信封時(shí)可以放下另一個(gè)信封。求最大的信封嵌套層數(shù)。
樣例
給一些信封 [[5,4],[6,4],[6,7],[2,3]] ,最大的信封嵌套層數(shù)是 3([2,3] => [5,4] => [6,7])。


給你一個(gè)二維整數(shù)數(shù)組 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 個(gè)信封的寬度和高度。當(dāng)另一個(gè)信封的寬度和高度都比這個(gè)信封大的時(shí)候,這個(gè)信封就可以放進(jìn)另一個(gè)信封里,如同俄羅斯套娃一樣。請(qǐng)計(jì)算 最多能有多少個(gè) 信封能組成一組“俄羅斯套娃”信封(即可以把一個(gè)信封放到另一個(gè)信封里面)。
注意:不允許旋轉(zhuǎn)信封。
示例 1:
輸入:envelopes = [[5,4],[6,4],[6,7],[2,3]]
輸出:3
解釋:最多信封的個(gè)數(shù)為 3, 組合為: [2,3] => [5,4] => [6,7]。
示例 2:
輸入:envelopes = [[1,1],[1,1],[1,1]]
輸出:1

前言

image.png

動(dòng)態(tài)規(guī)劃

image.png
class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        if (envelopes.empty()) {
            return 0;
        }
        
        int n = envelopes.size();
        sort(envelopes.begin(), envelopes.end(), [](const auto& e1, const auto& e2) {
            return e1[0] < e2[0] || (e1[0] == e2[0] && e1[1] > e2[1]);
        });

        vector<int> f(n, 1);
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < i; ++j) {
                if (envelopes[j][1] < envelopes[i][1]) {
                    f[i] = max(f[i], f[j] + 1);
                }
            }
        }
        return *max_element(f.begin(), f.end());
    }
};
class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        if not envelopes:
            return 0
        
        n = len(envelopes)
        envelopes.sort(key=lambda x: (x[0], -x[1]))

        f = [1] * n
        for i in range(n):
            for j in range(i):
                if envelopes[j][1] < envelopes[i][1]:
                    f[i] = max(f[i], f[j] + 1)
        
        return max(f)

復(fù)雜度分析

  • 時(shí)間復(fù)雜度:O(n^2),其中 nn 是數(shù)組 \textit{envelopes}envelopes 的長(zhǎng)度,排序需要的時(shí)間復(fù)雜度為O(nlogn),動(dòng)態(tài)規(guī)劃需要的時(shí)間復(fù)雜度為 O(n^2)O,前者在漸近意義下小于后者,可以忽略。
  • 空間復(fù)雜度:O(n)O(n),即為數(shù)組 ff 需要的空間。

基于二分查找的動(dòng)態(tài)規(guī)劃

image.png
class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        if (envelopes.empty()) {
            return 0;
        }
        
        int n = envelopes.size();
        sort(envelopes.begin(), envelopes.end(), [](const auto& e1, const auto& e2) {
            return e1[0] < e2[0] || (e1[0] == e2[0] && e1[1] > e2[1]);
        });

        vector<int> f = {envelopes[0][1]};
        for (int i = 1; i < n; ++i) {
            if (int num = envelopes[i][1]; num > f.back()) {
                f.push_back(num);
            }
            else {
                auto it = lower_bound(f.begin(), f.end(), num);
                *it = num;
            }
        }
        return f.size();
    }
};
class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        if not envelopes:
            return 0
        
        n = len(envelopes)
        envelopes.sort(key=lambda x: (x[0], -x[1]))

        f = [envelopes[0][1]]
        for i in range(1, n):
            if (num := envelopes[i][1]) > f[-1]:
                f.append(num)
            else:
                index = bisect.bisect_left(f, num)
                f[index] = num
        
        return len(f)

復(fù)雜度分析

  • 時(shí)間復(fù)雜度:O(nlogn),其中 nn 是數(shù)組 \textit{envelopes}envelopes 的長(zhǎng)度,排序需要的時(shí)間復(fù)雜度為O(nlogn),動(dòng)態(tài)規(guī)劃需要的時(shí)間復(fù)雜度同樣為 O(nlogn)。

  • 空間復(fù)雜度:O(n),即為數(shù)組 ff 需要的空間。

from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        size = len(envelopes)
        #print(envelopes)
        if size < 2:  # if the size of envelopes is smaller than 2, return
            return size
        envelopes.sort(key=lambda x: x[0])  # sort the envelopes according the width
        dp = [1 for _ in range(size)]  #record the number of envelope layers
        #print(dp)
        for i in range(1, size):
            for j in range(i):
                # compare the width and length of envelopes
                if envelopes[j][0] < envelopes[i][0] and envelopes[j][1] < envelopes[i][1]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

if __name__ == '__main__':
    envelopes = [[5,4],[6,4],[6,7],[2,3],[99,100]]  #input the matrix of envelopes
    solution = Solution()
    res = solution.maxEnvelopes(envelopes)
    print("The maximum envelope nesting layer is %d" %(res))

四、codetest4

反向部分鏈表元素(LeetCode中等難度;僅可通過(guò)常規(guī)的鏈表指針操作實(shí)現(xiàn)、僅可遍歷一次、不可通過(guò)交換鏈表指向的值來(lái)完成)。
給你單鏈表的頭指針 head 和兩個(gè)整數(shù) left 和 right ,其中 left <= right 。請(qǐng)你反轉(zhuǎn)從位置 left 到位置 right 的鏈表節(jié)點(diǎn),返回 反轉(zhuǎn)后的鏈表 。

前言

鏈表的操作問(wèn)題,一般而言面試(機(jī)試)的時(shí)候不允許我們修改節(jié)點(diǎn)的值,而只能修改節(jié)點(diǎn)的指向操作。思路通常都不難,寫對(duì)鏈表問(wèn)題的技巧是:一定要先想清楚思路,并且必要的時(shí)候在草稿紙上畫圖,理清「穿針引線」的先后步驟,然后再編碼。

方法一:穿針引線

我們以下圖中黃色區(qū)域的鏈表反轉(zhuǎn)為例。


image.png

使用「206. 反轉(zhuǎn)鏈表」的解法,反轉(zhuǎn) left 到 right 部分以后,再拼接起來(lái)。我們還需要記錄 left 的前一個(gè)節(jié)點(diǎn),和 right 的后一個(gè)節(jié)點(diǎn)。如圖所示:


image.png
算法步驟:
  • 先將待反轉(zhuǎn)的區(qū)域反轉(zhuǎn);
  • 把 pre 的 next 指針指向反轉(zhuǎn)以后的鏈表頭節(jié)點(diǎn),把反轉(zhuǎn)以后的鏈表的尾節(jié)點(diǎn)的 next 指針指向 succ。


    image.png

    說(shuō)明:編碼細(xì)節(jié)我們不在題解中介紹了,請(qǐng)見(jiàn)下方代碼。思路想明白以后,編碼不是一件很難的事情。這里要提醒大家的是,鏈接什么時(shí)候切斷,什么時(shí)候補(bǔ)上去,先后順序一定要想清楚,如果想不清楚,可以在紙上模擬,讓思路清晰。

class Solution {
private:
    void reverseLinkedList(ListNode *head) {
        // 也可以使用遞歸反轉(zhuǎn)一個(gè)鏈表
        ListNode *pre = nullptr;
        ListNode *cur = head;

        while (cur != nullptr) {
            ListNode *next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = next;
        }
    }

public:
    ListNode *reverseBetween(ListNode *head, int left, int right) {
        // 因?yàn)轭^節(jié)點(diǎn)有可能發(fā)生變化,使用虛擬頭節(jié)點(diǎn)可以避免復(fù)雜的分類討論
        ListNode *dummyNode = new ListNode(-1);
        dummyNode->next = head;

        ListNode *pre = dummyNode;
        // 第 1 步:從虛擬頭節(jié)點(diǎn)走 left - 1 步,來(lái)到 left 節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)
        // 建議寫在 for 循環(huán)里,語(yǔ)義清晰
        for (int i = 0; i < left - 1; i++) {
            pre = pre->next;
        }

        // 第 2 步:從 pre 再走 right - left + 1 步,來(lái)到 right 節(jié)點(diǎn)
        ListNode *rightNode = pre;
        for (int i = 0; i < right - left + 1; i++) {
            rightNode = rightNode->next;
        }

        // 第 3 步:切斷出一個(gè)子鏈表(截取鏈表)
        ListNode *leftNode = pre->next;
        ListNode *curr = rightNode->next;

        // 注意:切斷鏈接
        pre->next = nullptr;
        rightNode->next = nullptr;

        // 第 4 步:同第 206 題,反轉(zhuǎn)鏈表的子區(qū)間
        reverseLinkedList(leftNode);

        // 第 5 步:接回到原來(lái)的鏈表中
        pre->next = rightNode;
        leftNode->next = curr;
        return dummyNode->next;
    }
};
class Solution:
    def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode:
        def reverse_linked_list(head: ListNode):
            # 也可以使用遞歸反轉(zhuǎn)一個(gè)鏈表
            pre = None
            cur = head
            while cur:
                next = cur.next
                cur.next = pre
                pre = cur
                cur = next

        # 因?yàn)轭^節(jié)點(diǎn)有可能發(fā)生變化,使用虛擬頭節(jié)點(diǎn)可以避免復(fù)雜的分類討論
        dummy_node = ListNode(-1)
        dummy_node.next = head
        pre = dummy_node
        # 第 1 步:從虛擬頭節(jié)點(diǎn)走 left - 1 步,來(lái)到 left 節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)
        # 建議寫在 for 循環(huán)里,語(yǔ)義清晰
        for _ in range(left - 1):
            pre = pre.next

        # 第 2 步:從 pre 再走 right - left + 1 步,來(lái)到 right 節(jié)點(diǎn)
        right_node = pre
        for _ in range(right - left + 1):
            right_node = right_node.next
        # 第 3 步:切斷出一個(gè)子鏈表(截取鏈表)
        left_node = pre.next
        curr = right_node.next

        # 注意:切斷鏈接
        pre.next = None
        right_node.next = None

        # 第 4 步:同第 206 題,反轉(zhuǎn)鏈表的子區(qū)間
        reverse_linked_list(left_node)
        # 第 5 步:接回到原來(lái)的鏈表中
        pre.next = right_node
        left_node.next = curr
        return dummy_node.next

復(fù)雜度分析

  • 時(shí)間復(fù)雜度:O(N)O(N),其中 NN 是鏈表總節(jié)點(diǎn)數(shù)。最壞情況下,需要遍歷整個(gè)鏈表。
  • 空間復(fù)雜度:O(1)O(1)。只使用到常數(shù)個(gè)變量。

方法二:一次遍歷「穿針引線」反轉(zhuǎn)鏈表(頭插法)

方法一的缺點(diǎn)是:如果 left 和 right 的區(qū)域很大,恰好是鏈表的頭節(jié)點(diǎn)和尾節(jié)點(diǎn)時(shí),找到 left 和 right 需要遍歷一次,反轉(zhuǎn)它們之間的鏈表還需要遍歷一次,雖然總的時(shí)間復(fù)雜度為 O(N)O(N),但遍歷了鏈表 22 次,可不可以只遍歷一次呢?答案是可以的。我們依然畫圖進(jìn)行說(shuō)明。
我們依然以方法一的示例為例進(jìn)行說(shuō)明。


image.png

整體思想是:在需要反轉(zhuǎn)的區(qū)間里,每遍歷到一個(gè)節(jié)點(diǎn),讓這個(gè)新節(jié)點(diǎn)來(lái)到反轉(zhuǎn)部分的起始位置。下面的圖展示了整個(gè)流程。


image.png

下面我們具體解釋如何實(shí)現(xiàn)。使用三個(gè)指針變量 pre、curr、next 來(lái)記錄反轉(zhuǎn)的過(guò)程中需要的變量,它們的意義如下:
  • curr:指向待反轉(zhuǎn)區(qū)域的第一個(gè)節(jié)點(diǎn) left;
  • next:永遠(yuǎn)指向 curr 的下一個(gè)節(jié)點(diǎn),循環(huán)過(guò)程中,curr 變化以后 next 會(huì)變化;
  • pre:永遠(yuǎn)指向待反轉(zhuǎn)區(qū)域的第一個(gè)節(jié)點(diǎn) left 的前一個(gè)節(jié)點(diǎn),在循環(huán)過(guò)程中不變。

第 1 步,我們使用 ①、②、③ 標(biāo)注「穿針引線」的步驟。


image.png

操作步驟:

  • 先將 curr 的下一個(gè)節(jié)點(diǎn)記錄為 next;
  • 執(zhí)行操作 ①:把 curr 的下一個(gè)節(jié)點(diǎn)指向 next 的下一個(gè)節(jié)點(diǎn);
  • 執(zhí)行操作 ②:把 next 的下一個(gè)節(jié)點(diǎn)指向 pre 的下一個(gè)節(jié)點(diǎn);
  • 執(zhí)行操作 ③:把 pre 的下一個(gè)節(jié)點(diǎn)指向 next。
    第 1 步完成以后「拉直」的效果如下:
    image.png

    第 2 步,同理。同樣需要注意 「穿針引線」操作的先后順序
    image.png

    第 2 步完成以后「拉直」的效果如下:
    image.png

    第 3 步,同理。
    image.png

    第 3 步完成以后「拉直」的效果如下:
    image.png
class Solution {
public:
    ListNode *reverseBetween(ListNode *head, int left, int right) {
        // 設(shè)置 dummyNode 是這一類問(wèn)題的一般做法
        ListNode *dummyNode = new ListNode(-1);
        dummyNode->next = head;
        ListNode *pre = dummyNode;
        for (int i = 0; i < left - 1; i++) {
            pre = pre->next;
        }
        ListNode *cur = pre->next;
        ListNode *next;
        for (int i = 0; i < right - left; i++) {
            next = cur->next;
            cur->next = next->next;
            next->next = pre->next;
            pre->next = next;
        }
        return dummyNode->next;
    }
};
class Solution:
    def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode:
        # 設(shè)置 dummyNode 是這一類問(wèn)題的一般做法
        dummy_node = ListNode(-1)
        dummy_node.next = head
        pre = dummy_node
        for _ in range(left - 1):
            pre = pre.next

        cur = pre.next
        for _ in range(right - left):
            next = cur.next
            cur.next = next.next
            next.next = pre.next
            pre.next = next
        return dummy_node.next

復(fù)雜度分析:

  • 時(shí)間復(fù)雜度:O(N)O(N),其中 NN 是鏈表總節(jié)點(diǎn)數(shù)。最多只遍歷了鏈表一次,就完成了反轉(zhuǎn)。
  • 空間復(fù)雜度:O(1)O(1)。只使用到常數(shù)個(gè)變量。

五、codetest5

卷積加速

1、常規(guī)for循環(huán)

這樣會(huì)引入串行for循環(huán)逐一像素進(jìn)行操作,然后在每一個(gè)窗口大小的矩陣上進(jìn)行卷積運(yùn)算,效率太低。下面擴(kuò)展到3通道卷積操作,利用numpy切片同時(shí)計(jì)算3個(gè)通道。

def convolve2d(arr, kernel, stride=1, padding='same'):
    '''
    Using convolution kernel to smooth image

    Parameters
    ===========
    arr: 3D array or 3-channel image
    kernel: Filter matrix
    stride: Stride of scanning
    padding: padding mode
    '''
    h, w, channel = arr.shape
    k = kernel.shape[0]
    r = int(k/2)
    kernel_r = np.rot90(kernel,k=2,axes=(0,1))
    # padding outer area with 0
    padding_arr = np.zeros([h+k-1,w+k-1,channel])
    padding_arr[r:h+r,r:w+r] = arr 
    new_arr = np.zeros(arr.shape)
    for i in range(r,h+r,stride):
        for j in range(r,w+r,stride): 
            roi = padding_arr[i-r:i+r+1,j-r:j+r+1]
            new_arr[i-r,j-r] = np.sum(np.sum(roi*kernel_r,axis=0),axis=0)
    return new_arr[::stride,::stride]


if __name__=='__main__':
    A = np.arange(1,10001).reshape((100,100,1))
    print(A.shape)
    kernel = np.arange(1,10).reshape((3,3,1))/45
    # convert to 3-channels
    A3 = np.concatenate((A, 2*A, 3*A), axis=-1)
    k3 = np.concatenate((kernel, kernel, kernel), axis=-1)

    t1 = time.time()
    for i in range(100):
        A1 = convolve2d(A3,kernel,stride=2).astype(np.int)
    t2 = time.time()
    print(t2-t1)

###output
(100, 100, 1)
3.2025175

雖然不用循環(huán)計(jì)算多個(gè)通道,單通道與多通道耗時(shí)相同,但是依然需要for循環(huán)來(lái)滑動(dòng)窗口kernel,對(duì)于大圖片速度很慢,需要進(jìn)一步將外層循環(huán)替代。

向量化索引運(yùn)算

image.png
def convolve2d_vector(arr, kernel, stride=1, padding='same'):
    h, w, channel = arr.shape[0],arr.shape[1],arr.shape[2]
    k = kernel.shape[0]
    r = int(k/2)
    kernel_r = np.rot90(kernel,k=2,axes=(0,1))
    # padding outer area with 0
    padding_arr = np.zeros([h+k-1,w+k-1,channel])
    padding_arr[r:h+r,r:w+r] = arr 
    new_arr = np.zeros(arr.shape)

    vector = np.array(list(itertools.product(np.arange(r,h+r,stride),np.arange(r,w+r,stride))))
    vi = vector[:,0]
    vj = vector[:,1]  
    def _convolution(vi,vj):
        roi = padding_arr[vi-r:vi+r+1,vj-r:vj+r+1]
        new_arr[vi-r,vj-r] = np.sum(np.sum(roi*kernel_r,axis=0),axis=0)
    vfunc = np.vectorize(_convolution)    
    vfunc(vi,vj)
    return new_arr[::stride,::stride]

看來(lái)簡(jiǎn)單地使用 np.vectorize()并沒(méi)有實(shí)現(xiàn)向量化展開,必須對(duì)原圖的存儲(chǔ)結(jié)構(gòu)進(jìn)行重新排列和擴(kuò)展才能真正地利用向量化機(jī)制,即將窗口滑動(dòng)轉(zhuǎn)換為真正的矩陣運(yùn)算。

矩陣乘法運(yùn)算

正常的三通道卷積


image.png

轉(zhuǎn)換為矩陣相乘,對(duì)kernel和原圖進(jìn)行重新排列:


image.png

將卷積運(yùn)算轉(zhuǎn)化為矩陣乘法,從乘法和加法的運(yùn)算次數(shù)上看沒(méi)什么差別,但是轉(zhuǎn)化成矩陣后,可以在連續(xù)內(nèi)存和緩存上操作,而且有很多庫(kù)提供了高效的實(shí)現(xiàn)方法(BLAS、MKL),numpy內(nèi)部基于MKL實(shí)現(xiàn)運(yùn)算的加速。圖像展開消耗了更多的內(nèi)存,以空間換時(shí)間,另一方面,展開成矩陣形式可以轉(zhuǎn)換成CUDA代碼使用GPU加速。
image.png

上圖展示了3通道卷積的展開過(guò)程,RGB通道首位拼接,矩陣乘法運(yùn)算后的結(jié)果是三個(gè)通道卷積結(jié)果的累加,這里有所不同,我們需要單獨(dú)輸出每個(gè)通道的卷積結(jié)果而不累加,與原圖像大小相同,見(jiàn)下圖。


image.png

二維矩陣乘法采用np.dot()函數(shù),而高維數(shù)組乘法運(yùn)算采用np.matmul()函數(shù)。高維數(shù)組(n>2)相乘須滿足以下兩個(gè)條件:

兩個(gè)n維數(shù)組的前n-2維必須完全相同。例如(3,2,4,2)(3,2,2,3)前兩維必須完全一致;
最后兩維必須滿足二階矩陣乘法要求。例如(3,2,4,2)(3,2,2,3)的后兩維可視為(4,2)x(2,3)滿足矩陣乘法。
先利用reshape將kernel展開為(1,k*k,c),再用轉(zhuǎn)置函數(shù)transpose將channel前置,照此方法循環(huán)展開image矩陣。numpy的廣播規(guī)則會(huì)將kernel的第一個(gè)維度補(bǔ)齊,所以輸入kernel可以是1通道的(三通道共用一個(gè)卷積核)。

def convolve2d_vector(arr, kernel, stride=1, padding='same'):
    h, w, channel = arr.shape[0],arr.shape[1],arr.shape[2]
    k, n = kernel.shape[0], kernel.shape[2]
    r = int(k/2)
    #重新排列kernel為左乘矩陣,通道channel前置以便利用高維數(shù)組的矩陣乘法
    matrix_l = kernel.reshape((1,k*k,n)).transpose((2,0,1))
    padding_arr = np.zeros([h+k-1,w+k-1,channel])
    padding_arr[r:h+r,r:w+r] = arr
    #重新排列image為右乘矩陣,通道channel前置
    matrix_r = np.zeros((channel,k*k,h*w))
    for i in range(r,h+r,stride):
        for j in range(r,w+r,stride): 
            roi = padding_arr[i-r:i+r+1,j-r:j+r+1].reshape((k*k,1,channel)).transpose((2,0,1))
            matrix_r[:,:,(i-r)*w+j-r:(i-r)*w+j-r+1] = roi[:,::-1,:]        
    result = np.matmul(matrix_l, matrix_r)
    out = result.reshape((channel,h,w)).transpose((1,2,0))
    return out[::stride,::stride]

if __name__=='__main__':
    N=1000
    A = np.arange(1,N**2+1).reshape((N,N,1))
    print(A.shape)
    kernel = np.arange(3**2).reshape((3,3,1))/45
    # convert to 3-channels
    A3 = np.concatenate((A, 2*A, 3*A), axis=-1)
    k3 = np.concatenate((kernel, kernel, kernel), axis=-1)

    t1 = time.time()
    for i in range(1):
        B1 = convolve2d(A,kernel,stride=2).astype(np.int)
    t2 = time.time()
    print(t2-t1)

    t1 = time.time()
    for i in range(1):
        B2 = convolve2d_vector(A,kernel,stride=2).astype(np.int)
    t2 = time.time()
    print(t2-t1)
    print(B1.all()==B2.all())

###output
(1000, 1000, 1)
4.517540216445923
0.8763346672058105
True

可以看到運(yùn)算結(jié)果與convolve2d滑動(dòng)卷積的結(jié)果一致,且速度提升了5倍左右。速度主要瓶頸還是來(lái)自image的展開操作,這里還是滑動(dòng)窗口掃描實(shí)現(xiàn)的,for循環(huán)內(nèi)部進(jìn)行了局部拷貝操作,是串行的,數(shù)組切片和矩陣運(yùn)算部分是并行的。

總結(jié)

在CPU程序上優(yōu)化卷積運(yùn)算,numpy庫(kù)是個(gè)不錯(cuò)的選擇,初始算法將三通道計(jì)算和求和過(guò)程并行化,獲得了接近3倍的加速。轉(zhuǎn)換為矩陣/多維數(shù)組乘法后,速度進(jìn)一步提升了5倍。在python中用多線程和多進(jìn)程的方式加速for循環(huán)效果并不理想,CPU程序上創(chuàng)建和啟動(dòng)多線程開銷很大,進(jìn)程和線程通信、傳遞數(shù)據(jù)消耗了大部分時(shí)間,總體速度不升反降。

六、codetest6 sigmoid 反向傳播

class Network(object):
...
   def backprop(self, x, y):
        """Return a tuple "(nabla_b, nabla_w)" representing the
        gradient for the cost function C_x.  "nabla_b" and
        "nabla_w" are layer-by-layer lists of numpy arrays, similar
        to "self.biases" and "self.weights"."""
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # feedforward
        activation = x
        activations = [x] # list to store all the activations, layer by layer
        zs = [] # list to store all the z vectors, layer by layer
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # backward pass
        delta = self.cost_derivative(activations[-1], y) * \
            sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        # Note that the variable l in the loop below is used a little
        # differently to the notation in Chapter 2 of the book.  Here,
        # l = 1 means the last layer of neurons, l = 2 is the
        # second-last layer, and so on.  It's a renumbering of the
        # scheme in the book, used here to take advantage of the fact
        # that Python can use negative indices in lists.
        for l in xrange(2, self.num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())
        return (nabla_b, nabla_w)

...

    def cost_derivative(self, output_activations, y):
        """Return the vector of partial derivatives \partial C_x /
        \partial a for the output activations."""
        return (output_activations-y) 

def sigmoid(z):
    """The sigmoid function."""
    return 1.0/(1.0+np.exp(-z))

def sigmoid_prime(z):
    """Derivative of the sigmoid function."""
    return sigmoid(z)*(1-sigmoid(z))

https://zhuanlan.zhihu.com/p/78713744
https://zhuanlan.zhihu.com/p/260109670

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容