學(xué)習(xí)筆記CB010:遞歸神經(jīng)網(wǎng)絡(luò)、LSTM、自動抓取字幕

遞歸神經(jīng)網(wǎng)絡(luò)可存儲記憶神經(jīng)網(wǎng)絡(luò),LSTM是其中一種,在NLP領(lǐng)域應(yīng)用效果不錯(cuò)。

遞歸神經(jīng)網(wǎng)絡(luò)(RNN),時(shí)間遞歸神經(jīng)網(wǎng)絡(luò)(recurrent neural network),結(jié)構(gòu)遞歸神經(jīng)網(wǎng)絡(luò)(recursive neural network)。時(shí)間遞歸神經(jīng)網(wǎng)絡(luò)神經(jīng)元間連接構(gòu)成有向圖,結(jié)構(gòu)遞歸神經(jīng)網(wǎng)絡(luò)利用相似神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)遞歸構(gòu)造更復(fù)雜深度網(wǎng)絡(luò)。兩者訓(xùn)練屬同一算法變體。

時(shí)間遞歸神經(jīng)網(wǎng)絡(luò)。傳統(tǒng)神經(jīng)網(wǎng)絡(luò)FNN(Feed-Forward Neural Networks),前向反饋神經(jīng)網(wǎng)絡(luò)。RNN引入定向循環(huán),神經(jīng)元為節(jié)點(diǎn)組成有向環(huán),可表達(dá)前后關(guān)聯(lián)關(guān)系。隱藏層節(jié)點(diǎn)間構(gòu)成全連接,一個(gè)隱藏層節(jié)點(diǎn)輸出可作另一個(gè)隱藏層節(jié)點(diǎn)或自己的輸入。U、V、W是變換概率矩陣,x是輸入,o是輸出。RNN關(guān)鍵是隱藏層,隱藏層捕捉序列信息,記憶能力。RNN中U、V、W參數(shù)共享,每一步都在做相同事情,輸入不同,降低參數(shù)個(gè)數(shù)和計(jì)算量。RNN在NLP應(yīng)用較多,語言模型在已知已出現(xiàn)詞情況下預(yù)測下一個(gè)詞概率,是時(shí)序模型,下一個(gè)詞出現(xiàn)取決于前幾個(gè)詞,對應(yīng)RNN隱藏層間內(nèi)部連接。

RNN的訓(xùn)練方法。用BP誤差反向傳播算法更新訓(xùn)練參數(shù)。從輸入到輸出經(jīng)過步驟不確定,利用時(shí)序方式做前向計(jì)算,假設(shè)x表示輸入值,s表示輸入x經(jīng)過U矩陣變換后值,h表示隱藏層激活值,o表示輸出層值, f表示隱藏層激活函數(shù),g表示輸出層激活函數(shù)。當(dāng)t=0時(shí),輸入為x0, 隱藏層為h0。當(dāng)t=1時(shí),輸入為x1, s1 = Ux1+Wh0, h1 = f(s1), o1 = g(Vh1)。當(dāng)t=2時(shí),s2 = Ux2+Wh1, h2 = f(s2), o2 = g(Vh2)。st = Uxt + Wh(t-1), ht = f(st), ot = g(Vht)。h=f(現(xiàn)有的輸入+過去記憶總結(jié)),對RNN記憶能力全然體現(xiàn)。
UVW變換概率矩陣,x輸入,s xU矩陣變換后值,f隱藏層激活函數(shù),h隱藏層激活值,g輸出層激活函數(shù),o輸出。時(shí)間、輸入、變換(輸入、前隱藏)、隱藏(變換)、輸出(隱藏)。輸出(隱藏(變換(時(shí)間、輸入、前隱藏)))。反向修正參數(shù),每一步輸出o和實(shí)際o值誤差,用誤差反向推導(dǎo),鏈?zhǔn)角髮?dǎo)求每層梯度,更新參數(shù)。

LSTM(Long Short Tem Momery networks)。RNN存在長序列依賴(Long-Term Dependencies)問題。下一個(gè)詞出現(xiàn)概率和非常久遠(yuǎn)之前詞有關(guān),考慮到計(jì)算量,限制依賴長度。http://colah.github.io/posts/2015-08-Understanding-LSTMs 。傳統(tǒng)RNN示意圖,只包含一個(gè)隱藏層,tanh為激發(fā)函數(shù),“記憶”體現(xiàn)在t滑動窗口,有多少個(gè)t就有多少記憶。

LSTM設(shè)計(jì),神經(jīng)網(wǎng)絡(luò)層(權(quán)重系數(shù)和激活函數(shù),σ表示sigmoid激活函數(shù),tanh表示tanh激活函數(shù)),矩陣運(yùn)算(矩陣乘或矩陣加)。歷史信息傳遞和記憶,調(diào)大小閥門(乘以一個(gè)0到1之間系數(shù)),第一個(gè)sigmoid層計(jì)算輸出0到1之間系數(shù),作用到×門,這個(gè)操作表達(dá)上一階段傳遞過來的記憶保留多少,忘掉多少。忘掉記憶多少取決上一隱藏層輸出h{t-1}和本層的輸入x{t}。上一層輸出h{t-1}和本層的輸入x{t}得出新信息,存到記憶。計(jì)算輸出值Ct部分tanh神經(jīng)元和計(jì)算比例系數(shù)sigmoid神經(jīng)元(sigmoid取值范圍是[0,1]作比例系數(shù),tanh取值范圍[-1,1]作一個(gè)輸出值)。隱藏層輸出h計(jì)算,考慮當(dāng)前全部信息(上一時(shí)序隱藏層輸出、本層輸入x和當(dāng)前整體記憶信息),本單元狀態(tài)部分C通過tanh激活并做一個(gè)過濾(上一時(shí)序輸出值和當(dāng)前輸入值通過sigmoid激活系數(shù))。一句話詞是不同時(shí)序輸入x,在某一時(shí)間t出現(xiàn)詞A概率可LSTM計(jì)算,詞A出現(xiàn)概率取決前面出現(xiàn)過詞,取決前面多少個(gè)詞不確定,LSTM存儲記憶信息C,得出較接近概率。

聊天機(jī)器人是范問答系統(tǒng)。

語料庫獲取。范問答系統(tǒng),一般從互聯(lián)網(wǎng)收集語料信息,比如百度、谷歌,構(gòu)建問答對組成語料庫。語料庫分成多訓(xùn)練集、開發(fā)集、測試集。問答系統(tǒng)訓(xùn)練在一堆答案里找一個(gè)正確答案模型。訓(xùn)練過程不把所有答案都放到一個(gè)向量空間,做分組,在語料庫里采集樣本,收集每一個(gè)問題對應(yīng)500個(gè)答案集合,500個(gè)里面有正向樣本,隨機(jī)選些負(fù)向樣本,突出正向樣本作用。

基于CNN系統(tǒng)設(shè)計(jì),sparse interaction(稀疏交互),parameter sharing(參數(shù)共享),equivalent respresentation(等價(jià)表示),適合自動問答系統(tǒng)答案選擇模型訓(xùn)練。

通用訓(xùn)練方法。訓(xùn)練時(shí)獲取問題詞向量Vq(詞向量可用google word2vec訓(xùn)練,和一個(gè)正向答案詞向量Va+,和一個(gè)負(fù)向答案詞向量Va-, 比較問題和兩個(gè)答案相似度,兩個(gè)相似度差值大于一個(gè)閾值m更新模型參數(shù),在候選池里選答案,小于m不更新模型。參數(shù)更新,梯度下降、鏈?zhǔn)角髮?dǎo)。測試數(shù)據(jù),計(jì)算問題和候選答案cos距離,相似度最大是正確答案預(yù)測。

神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)設(shè)計(jì)。HL hide layer隱藏層,激活函數(shù)z = tanh(Wx+B),CNN 卷積層,P 池化層,池化步長 1,T tanh層,P+T輸出是向量表示,最終輸出兩個(gè)向量cos相似度。HL或CNN連起來表示共享相同權(quán)重。CNN輸出維數(shù)取決做多少卷積特征。論文《Applying Deep Learning To Answer Selection- A Study And An Open Task》。

深度學(xué)習(xí)運(yùn)用到聊天機(jī)器人中,1. 神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)選擇、組合、優(yōu)化。2. 自然語言處理,機(jī)器識別詞向量。3. 相似或匹配關(guān)系考慮相似度計(jì)算,典型方法 cos距離。4. 文本序列全局信息用CNN或LSTM。5. 精度不高可加層。6. 計(jì)算量過大,參數(shù)共享和池化。

聊天機(jī)器人學(xué)習(xí),需要海量聊天語料庫。美劇字幕。外文電影或電視劇字幕文件是天然聊天語料,對話比較多美劇最佳。字幕庫網(wǎng)站www.zimuku.net。

自動抓取字幕。抓取器代碼(https://github.com/warmheartli/ChatBotCourse)。在subtitle下創(chuàng)建目錄result,scrapy.Request
方法調(diào)用時(shí)增加傳參 dont_filter=True:

# coding:utf-8

import sys
import importlib
importlib.reload(sys)

import scrapy
from subtitle_crawler.items import SubtitleCrawlerItem

class SubTitleSpider(scrapy.Spider):
    name = "subtitle"
    allowed_domains = ["zimuku.net"]
    start_urls = [
            "http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=20",
            "http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=21",
            "http://www.zimuku.net/search?q=&t=onlyst&ad=1&p=22",
    ]

    def parse(self, response):
        hrefs = response.selector.xpath('//div[contains(@class, "persub")]/h1/a/@href').extract()
        for href in hrefs:
            url = response.urljoin(href)
            request = scrapy.Request(url, callback=self.parse_detail, dont_filter=True)
            yield request

    def parse_detail(self, response):
        url = response.selector.xpath('//li[contains(@class, "dlsub")]/div/a/@href').extract()[0]
        print("processing: ", url)
        request = scrapy.Request(url, callback=self.parse_file, dont_filter=True)
        yield request

    def parse_file(self, response):
        body = response.body
        item = SubtitleCrawlerItem()
        item['url'] = response.url
        item['body'] = body
        return item

# -*- coding: utf-8 -*-

class SubtitleCrawlerPipeline(object):
    def process_item(self, item, spider):
        url = item['url']
        file_name = url.replace('/','_').replace(':','_')+'.rar'
        fp = open('result/'+file_name, 'wb+')
        fp.write(item['body'])
        fp.close()
        return item

ls result/|head -1 , ls result/|wc -l , du -hs result/ 。

字幕文件解壓,linux直接執(zhí)行unzip file.zip。linux解壓rar文件,http://www.rarlab.com/download.htm 。wget http://www.rarlab.com/rar/rarlinux-x64-5.4.0.tar.gz 。tar zxvf rarlinux-x64-5.4.0.tar.gz
./rar/unrar 。解壓命令,unrar x file.rar 。linux解壓7z文件,http://downloads.sourceforge.net/project/p7zip 下載源文件,解壓執(zhí)行make編譯 bin/7za可用,用法 bin/7za x file.7z。

程序和腳本在https://github.com/warmheartli/ChatBotCourse 。第一步:爬取影視劇字幕。第二步:壓縮格式分類。文件多無法ls、文件名帶特殊字符、文件名重名誤覆蓋、擴(kuò)展名千奇百怪,python腳本mv_zip.py

import glob
import os
import fnmatch
import shutil
import sys

def iterfindfiles(path, fnexp):
    for root, dirs, files in os.walk(path):
        for filename in fnmatch.filter(files, fnexp):
            yield os.path.join(root, filename)

i=0
for filename in iterfindfiles(r"./input/", "*.ZIP"):
    i=i+1
    newfilename = "zip/" + str(i) + "_" + os.path.basename(filename)
    print(filename + " <===> " + newfilename)
    shutil.move(filename, newfilename)
    #sys.exit(-1)

擴(kuò)展名根據(jù)壓縮文件修改.rar、.RAR、.zip、.ZIP。第三步:解壓。根據(jù)操作系統(tǒng)下載不同解壓工具,建議unrar和unzip,腳本來實(shí)現(xiàn)批量解壓:

i=0; for file in `ls`; do mkdir output/${i}; echo "unzip $file -d output/${i}";unzip -P abc $file -d output/${i} > /dev/null; ((i++)); done
i=0; for file in `ls`; do mkdir output/${i}; echo "${i} unrar x $file output/${i}";unrar x $file output/${i} > /dev/null; ((i++)); done

第四步:srt、ass、ssa字幕文件分類整理。字幕文件類型srt、lrc、ass、ssa、sup、idx、str、vtt。第五步:清理目錄。自動清理空目錄腳本clear_empty_dir.py :

import glob
import os
import fnmatch
import shutil
import sys

def iterfindfiles(path, fnexp):
    for root, dirs, files in os.walk(path):
        if 0 == len(files) and len(dirs) == 0:
            print(root)
            os.rmdir(root)

iterfindfiles(r"./input/", "*.srt")

第六步:清理非字幕文件。批量刪除腳本del_file.py :

import glob
import os
import fnmatch
import shutil
import sys

def iterfindfiles(path, fnexp):
    for root, dirs, files in os.walk(path):
        for filename in fnmatch.filter(files, fnexp):
            yield os.path.join(root, filename)

for suffix in ("*.mp4", "*.txt", "*.JPG", "*.htm", "*.doc", "*.docx", "*.nfo", "*.sub", "*.idx"):
    for filename in iterfindfiles(r"./input/", suffix):
        print(filename)
        os.remove(filename)

第七步:多層解壓縮。第八步:舍棄剩余少量文件。無擴(kuò)展名、特殊擴(kuò)展名、少量壓縮文件,總體不超過50M。第九步:編碼識別與轉(zhuǎn)碼。utf-8、utf-16、gbk、unicode、iso8859,統(tǒng)一utf-8,get_charset_and_conv.py :

import chardet
import sys
import os

if __name__ == '__main__':
    if len(sys.argv) == 2:
        for root, dirs, files in os.walk(sys.argv[1]):
            for file in files:
                file_path = root + "/" + file
                f = open(file_path,'r')
                data = f.read()
                f.close()
                encoding = chardet.detect(data)["encoding"]
                if encoding not in ("UTF-8-SIG", "UTF-16LE", "utf-8", "ascii"):
                    try:
                        gb_content = data.decode("gb18030")
                        gb_content.encode('utf-8')
                        f = open(file_path, 'w')
                        f.write(gb_content.encode('utf-8'))
                        f.close()
                    except:
                        print("except:", file_path)

第十步:篩選中文。extract_sentence_srt.py :

# coding:utf-8
import chardet
import os
import re

cn=ur"([u4e00-u9fa5]+)"
pattern_cn = re.compile(cn)
jp1=ur"([u3040-u309F]+)"
pattern_jp1 = re.compile(jp1)
jp2=ur"([u30A0-u30FF]+)"
pattern_jp2 = re.compile(jp2)

for root, dirs, files in os.walk("./srt"):
    file_count = len(files)
    if file_count > 0:
        for index, file in enumerate(files):
            f = open(root + "/" + file, "r")
            content = f.read()
            f.close()
            encoding = chardet.detect(content)["encoding"]
            try:
                for sentence in content.decode(encoding).split('n'):
                    if len(sentence) > 0:
                        match_cn =  pattern_cn.findall(sentence)
                        match_jp1 =  pattern_jp1.findall(sentence)
                        match_jp2 =  pattern_jp2.findall(sentence)
                        sentence = sentence.strip()
                        if len(match_cn)>0 and len(match_jp1)==0 and len(match_jp2) == 0 and len(sentence)>1 and len(sentence.split(' ')) < 10:
                            print(sentence.encode('utf-8'))
            except:
                continue

第十一步:字幕中句子提取。

# coding:utf-8
import chardet
import os
import re

cn=ur"([u4e00-u9fa5]+)"
pattern_cn = re.compile(cn)
jp1=ur"([u3040-u309F]+)"
pattern_jp1 = re.compile(jp1)
jp2=ur"([u30A0-u30FF]+)"
pattern_jp2 = re.compile(jp2)

for root, dirs, files in os.walk("./ssa"):
    file_count = len(files)
    if file_count > 0:
        for index, file in enumerate(files):
            f = open(root + "/" + file, "r")
            content = f.read()
            f.close()
            encoding = chardet.detect(content)["encoding"]
            try:
                for line in content.decode(encoding).split('n'):
                    if line.find('Dialogue') == 0 and len(line) < 500:
                        fields = line.split(',')
                        sentence = fields[len(fields)-1]
                        tag_fields = sentence.split('}')
                        if len(tag_fields) > 1:
                            sentence = tag_fields[len(tag_fields)-1]
                        match_cn =  pattern_cn.findall(sentence)
                        match_jp1 =  pattern_jp1.findall(sentence)
                        match_jp2 =  pattern_jp2.findall(sentence)
                        sentence = sentence.strip()
                        if len(match_cn)>0 and len(match_jp1)==0 and len(match_jp2) == 0 and len(sentence)>1 and len(sentence.split(' ')) < 10:
                            sentence = sentence.replace('N', '')
                            print(sentence.encode('utf-8'))
            except:
                continue

第十二步:內(nèi)容過濾。過濾特殊unicode字符、關(guān)鍵詞、去除字幕樣式標(biāo)簽、html標(biāo)簽、連續(xù)特殊字符、轉(zhuǎn)義字符、劇集信息:

# coding:utf-8
import sys
import re
import chardet

if __name__ == '__main__':
    #illegal=ur"([u2000-u2010]+)"
    illegal=ur"([u0000-u2010]+)"
    pattern_illegals = [re.compile(ur"([u2000-u2010]+)"), re.compile(ur"([u0090-u0099]+)")]
    filters = ["字幕", "時(shí)間軸:", "校對:", "翻譯:", "后期:", "監(jiān)制:"]
    filters.append("時(shí)間軸:")
    filters.append("校對:")
    filters.append("翻譯:")
    filters.append("后期:")
    filters.append("監(jiān)制:")
    filters.append("禁止用作任何商業(yè)盈利行為")
    filters.append("http")
    htmltagregex = re.compile(r'<[^>]+>',re.S)
    brace_regex = re.compile(r'{.*}',re.S)
    slash_regex = re.compile(r'\w',re.S)
    repeat_regex = re.compile(r'[-=]{10}',re.S)
    f = open("./corpus/all.out", "r")
    count=0
    while True:
        line = f.readline()
        if line:
            line = line.strip()

            # 編碼識別,不是utf-8就過濾
            gb_content = ''
            try:
                gb_content = line.decode("utf-8")
            except Exception as e:
                sys.stderr.write("decode error:  ", line)
                continue

            # 中文識別,不是中文就過濾
            need_continue = False
            for pattern_illegal in pattern_illegals:
                match_illegal = pattern_illegal.findall(gb_content)
                if len(match_illegal) > 0:
                    sys.stderr.write("match_illegal error: %sn" % line)
                    need_continue = True
                    break
            if need_continue:
                continue

            # 關(guān)鍵詞過濾
            need_continue = False
            for filter in filters:
                try:
                    line.index(filter)
                    sys.stderr.write("filter keyword of %s %sn" % (filter, line))
                    need_continue = True
                    break
                except:
                    pass
            if need_continue:
                continue

            # 去掉劇集信息
            if re.match('.*第.*季.*', line):
                sys.stderr.write("filter copora %sn" % line)
                continue
            if re.match('.*第.*集.*', line):
                sys.stderr.write("filter copora %sn" % line)
                continue
            if re.match('.*第.*幀.*', line):
                sys.stderr.write("filter copora %sn" % line)
                continue

            # 去html標(biāo)簽
            line = htmltagregex.sub('',line)

            # 去花括號修飾
            line = brace_regex.sub('', line)

            # 去轉(zhuǎn)義
            line = slash_regex.sub('', line)

            # 去重復(fù)
            new_line = repeat_regex.sub('', line)
            if len(new_line) != len(line):
                continue

            # 去特殊字符
            line = line.replace('-', '').strip()

            if len(line) > 0:
                sys.stdout.write("%sn" % line)
            count+=1
        else:
            break
    f.close()
    pass

參考資料:

《Python 自然語言處理》

http://www.shareditor.com/blogshow?blogId=103

http://www.shareditor.com/blogshow?blogId=104

http://www.shareditor.com/blogshow?blogId=105

http://www.shareditor.com/blogshow?blogId=112

歡迎推薦上海機(jī)器學(xué)習(xí)工作機(jī)會,我的微信:qingxingfengzi

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

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

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