你挡我一时,挡不了我一世的文章

  • 机器学习 24 、MF ANN

    前文链接:https://write-bug.com/article/2696.html
    MF(Matrix Factorization)基于矩阵分解的推荐算法-隐语义模型:ALS、LFM、SVD、SVD++
    在15年左右流行
    ALS:-交替最小二乘
    我们之前学习的协同过滤CF:单独考虑user、item
    这里同时考虑User-item两方面:
    原来我们的UI稀疏打分矩阵\<m,n>:

    一般公司用户量可以随意达到上亿,物品量如音乐可以达到几十万,用户量之所以多,是因为可能一个用户有多个账户等等,共同组成一个很大很稀疏的矩阵那么面对这样一个矩阵,我们可以通过矩阵分解来解决:
    将User和Item分别映射到新的空间,分解成两个矩阵,U 和I两个维度都不变



    K值远小于M和N,从而达到降维目的
    无需关注新空间的各个维度,只需假定存在 ,即用向量表示user和item
    新的维度称为Latent Factor(隐因子)

    K的维度相比原来来说很小很小,并且可以人为设定,只需要两个矩阵相乘就能得到UI矩阵,即R’ ≈R
    两个矩阵相似如何界定?误差
    误差表示:RMSE :均方根误差
    目标:真实矩阵,和结果矩阵之间的尽可能逼近
    最小化平方误差作为损失函数:

    考虑矩阵稳定性,引入L2正则项,得到损失函数:

    这里的rui就是上面所说的R,即原矩阵user对item打的分数
    xuyi即分解矩阵后再相乘的预估分数,两个相减就是误差
    未知数:
    Xu:user vectoryi:item vector如何求解最优值:求导=0
    公式1:

    导数为0,可得到:

    同理对yi求导(公式2):

    为什么叫交替二乘法?
    这里的做法是让x和y交替下降:

    随机生成X、Y向量(初始化)
    固定Y,更新X(公式1)
    固定X,更新Y(公式2)
    第2、3步循环,直到满足收敛条件(RMSE)

    ——均方根误差
    那么我们得到这两个小矩阵,可以做什么?
    item-item :IK*KI=IIUser-User:UK*KU=UUuser与item的向量
    LFM思路和ALS算法类似,区别在于,ALS利用坐标下降法,LFM利用梯度下降法
    假设:
    评分矩阵𝑅𝑚,𝑛,m个用户对n个物品评分

    𝑟𝑢,𝑖:用户u对物品i的评分
    𝑅𝑚,𝑛 = 𝑃𝑚,𝐹 ∙ 𝑄𝐹,𝑛:R是两个矩阵的乘积
    P:每一行代表一个用户对各隐因子的喜欢程度 ,即前面的user矩阵
    Q:每一列代表一个物品在各个隐因子上的概率分布,即前面的item矩阵


    在之前的随机梯度下降中,我们更新w(t+1)=w(t) - a*g(梯度)
    那么根据这样的思路,这里把矩阵P与Q的分数当作权重w来更新:

    矩阵展开后相当于对每个元素分数进行偏导:

    随意抽取中间这个元素作为代表,求取偏导:

    由于

    所以后面两个等式相等,之后就有一件很有意思的事情发生了:P的分数是由上一时刻的Q更新的,Q的分数是由上一时刻的P更新的。也就是类似上面的坐标下降法:交替进行。
    代入原式:

    LFM实践:
    class LFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F#维度K self.P = dict() self.Q = dict() self.alpha = alpha#学习率 self.lmbd = lmbd self.max_iter = max_iter#迭代轮数 self.rating_data = rating_data#矩阵 '''随机初始化矩阵P和Q''' for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] for item, _ in rates: if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F))if __name__ == '__main__': '''用户有A B C,物品有a b c d,列表模拟矩阵:''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = LFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 预测计算用户A对各个物品的喜好程度SVDLFM有什么缺点?没有考虑客观的偏置,所以带偏置的LFM称为SVD
    什么是偏置,比如说有的人很极端给一些物品很高或者很低的分数,而有的人给每个物品都很平均的分数,还有包括地区等等都会影响对物品的看法,所以就有一个偏置存在。
    偏置:事件固有的,不受外界影响的属性。

    𝜇:训练集中所有评分的平均值
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    更新:

    SVD实践:
    class BiasLFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P和Q''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0#初始化bu cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0#初始化bi self.mu /= cnt#计算μ def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui #更新两个b self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分,加偏置 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F)) + self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = BiasLFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 计算用户A对各个物品的喜好程度SVD++任何用户只要对物品i有过评分,无论评分多少,已经在一定程度上反映了用户对各个隐因子的喜好 程度𝑦𝑖 = (𝑦𝑖1,𝑦𝑖2,…,𝑦𝑖𝐹),y是物品携带的属性,什么意思?比如说A买了3个item,B买了3个item,每个item背后有一系列feature vector,那么我们用A买的3个item背后的fea向量相加(实际计算是学习更新出来的,不一定是相加)代表一个虚拟的物品A,间接表达了这个用户的偏好程度,同理得到向量B,那么对于这个每个人背后的虚拟物品向量,就是y

    所以这个打分是在P上又增加了一个类似于偏置的东西,并做了归一化

    𝑁(𝑢):用户u评价过的物品集合
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    SVD++实践:
    class SVDPP(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.Y = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P、Q、Y''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0 cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] if item not in self.Y:#比之前svd多加了一个y,初始化y self.Y[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0 self.mu /= cnt def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: z = [0.0 for f in xrange(self.F)] for item, _ in rates: for f in xrange(self.F): z[f] += self.Y[item][f]#用户观看过物品的评分集合加和,即虚拟物品向量,即∑Yjf ru = 1.0 / math.sqrt(1.0 * len(rates)) s = [0.0 for f in xrange(self.F)] for item, rui in rates: hat_rui = self.predict(user, item, rates) err_ui = rui - hat_rui self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): s[f] += self.Q[item][f] * err_ui#每个物品的信息和误差相乘的累加 self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * ( err_ui * (self.P[user][f] + z[f] * ru) - self.lmbd * self.Q[item][f]) for item, _ in rates: for f in xrange(self.F): #Y的更新 self.Y[item][f] += self.alpha * (s[f] * ru - self.lmbd * self.Y[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item, ratedItems): '''预测用户user对物品item的评分 ''' z = [0.0 for f in xrange(self.F)] for ri, _ in ratedItems: for f in xrange(self.F): z[f] += self.Y[ri][f] return sum( (self.P[user][f] + z[f] / math.sqrt(1.0 * len(ratedItems))) * self.Q[item][f] for f in xrange(self.F)) + \ self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = SVDPP(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item, rate_A)) # 计算用户A对各个物品的喜好程度ANNANN 多维空间检索算法,不完全是算法,是更偏工程的一种方法,时下正在流行的简单方式,从图像检索演化而来,复杂一点的方式一般使用DNN可以达到目的每个用户、物品背后都有自己的向量映射在多为空间的点上,我们的目标就是把这些向量全部映射在一个空间内,求user最近的item点
    稀疏场景适用物品召回:cb倒排(token)、cf倒排(user)
    —召回能力有限
    鲜花和巧克力在情人节的情况下可以关联起来,但是通过cb不能召回,通过cf需要很多很多用户共点击或者共现才会关联
    那么这里如何计算和user距离近的点?之前我们使用cos或jaccard,但是我不能把所有的物品都遍历一遍,计算量过大,所以就引出了ANN的近邻检索annoy
    Annoy目标:建立一个数据结构,在较短的时间内找到任何查询点的最近点,在精度允许的条件下通过牺牲准 确率来换取比暴力搜索要快的多的搜索速度
    先圈出一部分集合,遍历集合取top
    如果推荐的结果不理想,不喜欢,不是这个方法的问题,而是embedding方式的问题
    那么这个集合如何圈呢?
    方案:随机选择两个点,然后根据这两个点之间的连线确定一个可以垂直等分线段的超平面,灰色是两点的连 线,黑色是超平面

    从而由里面的点集合就构成了叶子节点,最终形成整棵树
    建立二叉树结构用于表示空间分布,每一个节点表示一个子空间
    假设,如果两个点在空间彼此靠近,任何超平面不会将他们分开,如果我们搜索空间中的任意一点,和其距离 近的点为推荐候选,通过对二叉树的遍历,可以完成
    重复上页步骤,继续分割 ,过程很像Kmeans要求:每个子节点包含数据不超过K个(10):


    如果落在这个空间内的节点候选太少怎么办?或者本来两个点距离很近,但是被两个空间分割导致不能计算,那么我们就拓宽区域、多建几棵树,每次建立长得样子都不太一样。
    annoy只是多维空间检索中的一种方法,其他还有:

    KD-Tree (KNN-开始不随机,直接找到全局最优,速度慢,建树慢)
    局部敏感哈希LSH
    HNSW
    OPQ等等

    ANN实践:
    annoy安装:pip
    import sysfrom annoy import AnnoyIndexa = AnnoyIndex(3)#建立3颗树a.add_item(0, [1, 0, 0])#添加用户和item点a.add_item(1, [0, 1, 0])a.add_item(2, [0, 0, 1])a.build(-1)print(a.get_nns_by_item(0, 100))#与0这个用户最近的top100集合print(a.get_nns_by_vector([1.0, 0.5, 0.5], 100))#与这个item vector 最近的top100集合下面这个代码是什么意思呢?随着我们候选集合圈的缩小,我们的计算量也在缩小,那么我们目标是让这个候选集合的top和全局的top尽量一致,也就是说要在计算量和召回率之间做一个权衡,那么全局的top我们就只能通过暴力遍历来知道答案了。
    from __future__ import print_functionimport random, timefrom annoy import AnnoyIndextry: xrangeexcept NameError: # Python 3 compat xrange = rangen, f = 100000, 40#初始化10w个点,40颗树t = AnnoyIndex(f)for i in xrange(n): v = [] for z in xrange(f): v.append(random.gauss(0, 1))#高斯分布初始化点 t.add_item(i, v)#添加在树里t.build(2 * f)t.save('test.tree')#保存树limits = [10, 100, 1000, 10000]#圈的大小k = 10#10个候选prec_sum = {}prec_n = 100time_sum = {}for i in xrange(prec_n): j = random.randrange(0, n)#随机选一个点作为用户 #print(j) closest = set(t.get_nns_by_item(j, k, n))#求取与这个用户j最近的全局top10 #print(closest) for limit in limits: t0 = time.time() toplist = t.get_nns_by_item(j, k, limit)#圈内的top10 T = time.time() - t0 found = len(closest.intersection(toplist))#用全局与圈内的top取交集 hitrate = 1.0 * found / k#准确率 #print(hitrate) prec_sum[limit] = prec_sum.get(limit, 0.0) + hitrate time_sum[limit] = time_sum.get(limit, 0.0) + T print(prec_sum[limit])for limit in limits: print('limit: %-9d precision: %6.2f%% avg time: %.6fs' % (limit, 100.0 * prec_sum[limit] / (i + 1), time_sum[limit] / (i + 1)))
    0  留言 2019-06-24 11:08:26
  • 机器学习 23 、BM25 Word2Vec

    前文链接:https://write-bug.com/article/2689.html
    word embedding什么是word embedding? 简单的来说就是将一个特征转换为一个向量。在推荐系统当中,我们经常会遇到离散特征,如userid、itemid。对于离散特征,我们一般的做法是将其转换为one-hot,但对于itemid这种离散特征,转换成one-hot之后维度非常高,但里面只有一个是1,其余都为0。这种情况下,我们的通常做法就是将其转换为embedding。
    word embedding为什么翻译成词嵌入模型?一个向量可以想象成多维空间,那么在多维空间里面找到一个点来代表这个词,相当于把这个词嵌入到了多维空间里,所以可以称为词嵌入模型。
    one-hot编码形式
    输入:
    apple on a apple tree – Bag of Words: ["apple", "on", "a", "tree"]– Word Embedding的输出就是每个word的向量表示
    最简单直接的是one-hot编码形式:那么每个word都对应了一种数值表示
    例如,apple对应的vector就是[1, 0, 0, 0],a对应的vector就是[0, 0, 1, 0]
    两类表达方式:基于频率:(本质:one-hot)
    Count vector:需要穷举所有单词,形成非常庞大、稀疏的矩阵,穷举时,计算每词的词频TF
    每个文档用词向量的组合来表示,每个词的权重用其出现的次数来表示
    假设语料库内容如下:
    – D1: He is a boy. – D2: She is a girl, good girl.那么可以构建如下2 × 7维的矩阵

    tfidf vector:同时考虑了TF与IDF
    IDF=log(N/n)。 – N代表语料库中文档的总数 – n代表某个word在几个文档中出现过;
    Co-Occurrence vector:只考虑滑动窗口内单词的共现,等于考虑了上下文之间的关系
    前面两类方法,每个字都是独立的,缺少了语义的学习
    例如: He is not lazy. He is intelligent. He is smart.
    如果Context Window大小为2,那么可以得到如下的共现矩阵:

    he和is计算方法(窗口共现):

    基于模型:
    CBOW
    Skip-Gram
    技术升级TF-IDF-》BM25
    Co-Occurrence-》WordtoVector
    BM25:定位:搜索引擎相关性评分
    通常我们在搜索信息时,搜索的句子为query,关联出的排序列表为item,即:
    query -》item
    那么可分词query-》token1 token2 token3
    同时可把item -》doc-》token token token
    词在文章中的评分:token1 -》doc :score1(tfidf)
    所以相关性评分query-》item:score1+score2+…
    公式可表示为:
    S(q,d)=∑Wi*R(qi,d)Wi用idf表示:

    N为索引中的全部文档数
    n(qi)为包含了qi的文档数
    相关性评分R:


    k1,k2,b为调节因子,一般k1=2,b=0.75
    b是调整文档长度对相关性影响的大小 • b越大,文档长度影响R越大,反之越小


    fi为qi在d中的出现频率 =TF
    qfi为qi在Query中的出现频率 =query中的TF

    在之前的tfidf中TF越大最后的分数也越大,而通常情况下,文章越长,TF大的可能性越大,没有考虑到文章长度的问题,这里BM25考虑到了这个问题,如果文章过长就要给定一定的打压,那么如何界定文章长度呢?除以avgdl,即所有文章平均长度

    dl为文档d的长度
    avgdl为所有文档的平均长度

    公式简化:
    绝大部分情况下,qi在Query中只会出现一次,即qfi=1


    BM25实践:
    1.gensim word2vec
    语料库-》每个词的50维向量即word embedding
    from gensim.models import word2vecmodel=word2vec.Word2Vec(sentences,size=50,window=5,min_count=1,workers=6)model.wv.save_word2vec_format(file_voc, binary=False)2.coumpute_idf
    每个词的idf
    file_corpus='../data/file_corpus.txt'file_voc='../data/voc.txt'file_idf='../data/idf.txt'class ComIdf(object): def __init__(self,file_corpus,file_voc,file_idf): self.file_corpus=file_corpus self.file_voc=file_voc self.file_idf=file_idf self.voc=load_voc(self.file_voc) self.corpus_data=self.load_corpus() self.N=len(self.corpus_data) def load_corpus(self): input_data = codecs.open(self.file_corpus, 'r', encoding='utf-8') return input_data.readlines() def com_idf(self,word): n = 0 for _,line in enumerate(self.corpus_data): n+=line.count(word) idf=math.log(1.0*self.N/n+1) return {word:idf} def parts(self): words=set(self.voc.keys()) multiprocessing.freeze_support() cores=multiprocessing.cpu_count() pool=multiprocessing.Pool(processes=cores-2) reuslt=pool.map(self.com_idf,words) idf_dict=dict() for r in reuslt: k=list(r.keys())[0] v=list(r.values())[0] idf_dict[k]=idf_dict.get(k,0)+v with codecs.open(self.file_idf,'w',encoding='utf-8') as f:f.write(json.dumps(idf_dict,ensure_ascii=False,indent=2,sort_keys=False))if __name__ == '__main__': t1 = time.time() IDF=ComIdf(file_corpus,file_voc,file_idf)IDF.parts()3.get_sentence
    分割句子,取出最常见句子1w个
    def get_sentence(): file_corpus=codecs.open('../data/file_corpus.txt','r',encoding='utf-8') file_sentence=codecs.open('../data/file_sentence.txt','w',encoding='utf-8') st=dict() for _,line in enumerate(file_corpus): line=line.strip() blocks=re_han.split(line) for blk in blocks: if re_han.match(blk) and len(blk)>10: st[blk]=st.get(blk,0)+1 st=sorted(st.items(),key=lambda x:x[1],reverse=True) for s in st[:10000]: file_sentence.write(s[0]+'\n') file_corpus.close() file_sentence.close()get_sentence()4.test
    各种算法计算打分:cos\idf\bm25\jaccard
    file_voc='./data/voc.txt'file_idf='./data/idf.txt'file_userdict='./data/medfw.txt'class SSIM(object): def __init__(self): t1 = time.time() self.voc=load_voc(file_voc) print("Loading word2vec vector cost %.3f seconds...\n" % (time.time() - t1)) t1 = time.time() self.idf=load_idf(file_idf) print("Loading idf data cost %.3f seconds...\n" % (time.time() - t1)) jieba.load_userdict(file_userdict) def M_cosine(self,s1,s2): s1_list=jieba.lcut(s1) s2_list=jieba.lcut(s2) v1=np.array([self.voc[s] for s in s1_list if s in self.voc]) v2=np.array([self.voc[s] for s in s2_list if s in self.voc]) v1=v1.sum(axis=0) v2=v2.sum(axis=0) sim=1-spatial.distance.cosine(v1,v2) return sim def M_idf(self,s1, s2): v1, v2 = [], [] s1_list = jieba.lcut(s1) s2_list = jieba.lcut(s2) for s in s1_list: idf_v = self.idf.get(s, 1) if s in self.voc: v1.append(1.0 * idf_v * self.voc[s]) for s in s2_list: idf_v = self.idf.get(s, 1) if s in self.voc: v2.append(1.0 * idf_v * self.voc[s]) v1 = np.array(v1).sum(axis=0) v2 = np.array(v2).sum(axis=0) sim = 1 - spatial.distance.cosine(v1, v2) return sim def M_bm25(self,s1, s2, s_avg=10, k1=2.0, b=0.75): bm25 = 0 s1_list = jieba.lcut(s1) for w in s1_list: idf_s = self.idf.get(w, 1) bm25_ra = s2.count(w) * (k1 + 1) bm25_rb = s2.count(w) + k1 * (1 - b + b * len(s2) / s_avg) bm25 += idf_s * (bm25_ra / bm25_rb) return bm25 def M_jaccard(self,s1, s2): s1 = set(s1) s2 = set(s2) ret1 = s1.intersection(s2) ret2 = s1.union(s2) jaccard = 1.0 * len(ret1)/ len(ret2) return jaccard def ssim(self,s1,s2,model='cosine'): if model=='idf': f_ssim=self.M_idf elif model=='bm25': f_ssim=self.M_bm25 elif model=='jaccard': f_ssim=self.M_jaccard else: f_ssim = self.M_cosine sim=f_ssim(s1,s2) return simsm=SSIM()ssim=sm.ssimdef test(): test_data=[u'临床表现及实验室检查即可做出诊断', u'面条汤等容易消化吸收的食物为佳', u'每天应该摄入足够的维生素A', u'视患者情况逐渐恢复日常活动', u'术前1天开始预防性运用广谱抗生素'] model_list=['cosine','idf','bm25','jaccard'] file_sentence=codecs.open('./data/file_sentence.txt','r',encoding='utf-8') train_data=file_sentence.readlines() for model in model_list: t1 = time.time() dataset=dict() result=dict() for s1 in test_data: dataset[s1]=dict() for s2 in train_data: s2=s2.strip() if s1!=s2: sim=similarity.ssim(s1,s2,model=model) dataset[s1][s2]=dataset[s1].get(s2,0)+sim for r in dataset: top=sorted(dataset[r].items(),key=lambda x:x[1],reverse=True) result[r]=top[0] with codecs.open('./data/test_result.txt','a') as f: f.write('--------------The result of %s method------------------\n '%model) f.write('\tThe computing cost %.3f seconds\n'% (time.time() - t1)) f.write(json.dumps(result, ensure_ascii=True, indent=2, sort_keys=False)) f.write('\n\n') file_sentence.close()test()word2vec继承了之前的窗口滑动思想,得到更好的语义理解
    涉及的技术点:
    – Hierarchical softmax分层softmax
    – Negative sampling负采样
    两种方式:

    CBOW:根据前后词,预测中间词
    Skip-Gram :根据中间词预测前后词,工业用的更多

    CBOW一句话包含了很多单词,时间窗口选择到了一个中间词,w3,假设左边右边各学习两个,中间词假设不知道,那么根据w1245猜测中间这个w3词,故意装作w3不知道做预测,那么w3就是中心词label,w1245就是周围词训练数据,随着时间窗口滑动,同理w4又继续成为了中间词,那么由此类推形成了很多训练样本


    Input layer:由one-hot编码的输入上下文{x1,…,xC}组成, 其中窗口大小为C,词汇表大小为V
    Hidden layer:隐藏层是N维的向量
    Output layer:被one-hot编码的输出单词y

    被one-hot编码的输入向量通过一个V×N维的权重矩阵W连接到隐藏层; 隐藏层通过一个N×V的权重矩阵W′连接到输出层
    不管输入多少词,相加并都映射为中间得隐层向量上,再做一个softmax输出
    Skip-Gram:CBOW的网络翻转
    样本怎么得到的?

    input word和output word都是one-hot编码的向量。最终模型的输出是一个概率分布

    softmax分类需要所有单词:这里是50002个单词,需要遍历一遍,那么每次计算都需要一定时间,不管是多次还是一次时间成本都很高,所以引出了分层softmax:
    Hierarchical softmax哈夫曼树

    是二叉树的一种特殊形式,又称为最优二叉树,其主要作用在于数据压缩和编码长度的优化
    定义:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度(WPL)达到最小,称这样的二叉 树为最优二叉树,也称为霍夫曼树(Huffman Tree)
    WPL:树的带权路径长度,所有叶子结点的带权路径长度之和

    让wpl最小,压缩数据信息
    主要用于编码:从根节点出发,向左为0,向右为1
    等长编码和哈夫曼编码


    生成:
    每个节点按权重大小从小到大排序
    两个最小的为叶子节点相加成为父节点并和其他最小的成两个叶子节点,最终形成哈夫曼树
    之前做softmax,50002个单词需要从头到尾计算一遍,把词表做成树,每个叶子节点都是一个单词,树的每个结点都是一个sigmoid,来了一个单词做一个lr二分类向左还是向右分类直至最后叶子节点为所要的词
    Negative sampling负采样
    思想:每次让一个训练样本仅更新一小部分权重
    需要离错误的样本更远

    不对所有输出神经元对应的权值进行更新,只是随机选取几个“negative”词,更新他们对应的权重。当然, 我们也会更新”positive”的对应权值
    随机取的样本只要不共现就是负样本,概率大的会多选,即出现多的单词自然被取出的概率越大
    实际论文实现:


    CBOW:input是context(周围词)而output是中心词,训练过程中其实是在从output的loss学习周围词的信息 也就是embedding,但是在中间层是average的,一共预测V(vocab size)次就够了
    一次softmax

    Skipgram:用中心词预测周围词,预测的时候是一对word pair,等于对每一个中心词都有K个词作为output, 对于一个词的预测有K次,所以能够更有效的从context中学习信息,但是总共预测K*V词
    多次softmax

    Skip-Gram的训练时间更长,更准确
    Cbow训练速度快,Skip-Gram更好处理生僻字

    Word2Vec实践:
    import sysimport torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.utils.data as tudfrom torch.nn.parameter import Parameterfrom collections import Counterimport numpy as npimport randomimport mathimport pandas as pdimport scipyimport sklearnfrom sklearn.metrics.pairwise import cosine_similarity# 超参数K = 100 # number of negative samplesC = 3 # nearby words thresholdNUM_EPOCHS = 2 # The number of epochs of trainingMAX_VOCAB_SIZE = 30000 # the vocabulary sizeBATCH_SIZE = 128 # the batch sizeLEARNING_RATE = 0.2 # the initial learning rateEMBEDDING_SIZE = 100# 分词def word_tokenize(text): return text.split()with open("./data/text8", "r") as fin: text = fin.read()text = [w for w in word_tokenize(text.lower())]print(text[1])vocab = dict(Counter(text).most_common(MAX_VOCAB_SIZE - 1))#计数器,取前30000个高频元素vocab["<unk>"] = len(text) - np.sum(list(vocab.values()))print (np.sum(list(vocab.values())))print (len(text))print (vocab["<unk>"])print (len(vocab))#output:originated170052071700520769010330000idx_to_word = [word for word in vocab.keys()]word_to_idx = {word: i for i, word in enumerate(idx_to_word)}word_counts = np.array([count for count in vocab.values()], dtype=np.float32)word_freqs = word_counts / np.sum(word_counts)word_freqs = word_freqs ** (3. / 4.)word_freqs = word_freqs / np.sum(word_freqs) # 用来做 negative samplingVOCAB_SIZE = len(idx_to_word)print(VOCAB_SIZE)#text 每个单词#word_to_idx 单词id化:dict#idx_to_word 每个单词#word_freqs 负采样概率#word_counts 单词计数列表#数据样本列举:class WordEmbeddingDataset(tud.Dataset): def __init__(self, text, word_to_idx, idx_to_word, word_freqs, word_counts): ''' text: a list of words, all text from the training dataset word_to_idx: the dictionary from word to idx idx_to_word: idx to word mapping word_freq: the frequency of each word word_counts: the word counts ''' super(WordEmbeddingDataset, self).__init__() self.text_encoded = [word_to_idx.get(t, VOCAB_SIZE - 1) for t in text] self.text_encoded = torch.Tensor(self.text_encoded).long() self.word_to_idx = word_to_idx self.idx_to_word = idx_to_word self.word_freqs = torch.Tensor(word_freqs) self.word_counts = torch.Tensor(word_counts) def __len__(self): ''' 返回整个数据集(所有单词)的长度 ''' return len(self.text_encoded) def __getitem__(self, idx): ''' 这个function返回以下数据用于训练 - 中心词 - 这个单词附近的(positive)单词 - 随机采样的K个单词作为negative sample ''' center_word = self.text_encoded[idx] pos_indices = list(range(idx - C, idx)) + list(range(idx + 1, idx + C + 1)#窗口左3右3 pos_indices = [i % len(self.text_encoded) for i in pos_indices]#去除类似第一次左边无词这种情况 pos_words = self.text_encoded[pos_indices] neg_words = torch.multinomial(self.word_freqs, K * pos_words.shape[0], True) return center_word, pos_words, neg_wordsdataset = WordEmbeddingDataset(text, word_to_idx, idx_to_word, word_freqs, word_counts)dataloader = tud.DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)
    class EmbeddingModel(nn.Module): def __init__(self, vocab_size, embed_size): ''' 初始化输出和输出embedding ''' super(EmbeddingModel, self).__init__() self.vocab_size = vocab_size self.embed_size = embed_size initrange = 0.5 / self.embed_size self.out_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False) self.out_embed.weight.data.uniform_(-initrange, initrange) self.in_embed = nn.Embedding(self.vocab_size, self.embed_size, sparse=False) self.in_embed.weight.data.uniform_(-initrange, initrange) def forward(self, input_labels, pos_labels, neg_labels): ''' input_labels: 中心词, [batch_size] pos_labels: 中心词周围 context window 出现过的单词 [batch_size * (window_size * 2)] neg_labelss: 中心词周围没有出现过的单词,从 negative sampling 得到 [batch_size, (window_size * 2 * K)] return: loss ''' input_embedding = self.in_embed(input_labels) # B * embed_size pos_embedding = self.out_embed(pos_labels) # B * (2*C) * embed_size neg_embedding = self.out_embed(neg_labels) # B * (2*C * K) * embed_size print("input_embedding size:", input_embedding.size()) print("pos_embedding size:", pos_embedding.size()) print("neg_embedding size:", neg_embedding.size()) log_pos = torch.bmm(pos_embedding, input_embedding.unsqueeze(2)).squeeze() # B * (2*C) log_neg = torch.bmm(neg_embedding, -input_embedding.unsqueeze(2)).squeeze() # B * (2*C*K)先拓展再压缩 print("log_pos size:", log_pos.size()) print("log_neg size:", log_neg.size()) log_pos = F.logsigmoid(log_pos).sum(1)#1为横向压缩,2为竖向压缩,可以试试玩 log_neg = F.logsigmoid(log_neg).sum(1) loss = log_pos + log_neg print("log_pos size:", log_pos.size()) print("log_neg size:", log_neg.size()) print("loss size:", loss.size()) return -loss def input_embeddings(self): return self.in_embed.weight.data.cpu().numpy()model = EmbeddingModel(VOCAB_SIZE, EMBEDDING_SIZE)if USE_CUDA: model = model.cuda()if __name__ == '__main__': optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE) for e in range(NUM_EPOCHS): for i, (input_labels, pos_labels, neg_labels) in enumerate(dataloader): print(input_labels.size()) print(pos_labels.size()) print(neg_labels.size()) input_labels = input_labels.long() pos_labels = pos_labels.long() neg_labels = neg_labels.long() if USE_CUDA: input_labels = input_labels.cuda() pos_labels = pos_labels.cuda() neg_labels = neg_labels.cuda() optimizer.zero_grad() loss = model(input_labels, pos_labels, neg_labels).mean() loss.backward() optimizer.step() if i % 100 == 0: with open(LOG_FILE, "a") as fout: fout.write("epoch: {}, iter: {}, loss: {}\n".format(e, i, loss.item())) print("epoch: {}, iter: {}, loss: {}".format(e, i, loss.item())) embedding_weights = model.input_embeddings() torch.save(model.state_dict(), "embedding-{}.th".format(EMBEDDING_SIZE))使用场景:
    1.item title检索 +ANN+word avg,word average: – 对所有输入词向量求和并取平均 – 例如:输入三个4维词向量:(1,2,3,4),(9,6,11,8),(5,10,7,12) – 那么我们word2vec映射后的词向量就是(5,6,7,8)
    2.模型初始化:处理特征等
    3.近义词(语义理解):可通过计算词向量的加减,与相似度可进行语义理解
    实践例:
    def evaluate(filename, embedding_weights): if filename.endswith(".csv"): data = pd.read_csv(filename, sep=",") else: data = pd.read_csv(filename, sep="\t") human_similarity = [] model_similarity = [] for i in data.iloc[:, 0:2].index: word1, word2 = data.iloc[i, 0], data.iloc[i, 1] if word1 not in word_to_idx or word2 not in word_to_idx: continue else: word1_idx, word2_idx = word_to_idx[word1], word_to_idx[word2] word1_embed, word2_embed = embedding_weights[[word1_idx]], embedding_weights[[word2_idx]] model_similarity.append(float(sklearn.metrics.pairwise.cosine_similarity(word1_embed, word2_embed))) human_similarity.append(float(data.iloc[i, 2])) return scipy.stats.spearmanr(human_similarity, model_similarity)# , model_similaritydef find_nearest(word): index = word_to_idx[word] embedding = embedding_weights[index] cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])#1-cosine return [idx_to_word[i] for i in cos_dis.argsort()[:10]]model.load_state_dict(torch.load("model/embedding-{}.th".format(EMBEDDING_SIZE), map_location='cpu'))embedding_weights = model.input_embeddings()# 找相似的单词for word in ["good", "computer", "china", "mobile", "study"]: print(word, find_nearest(word))# 单词之间的关系,寻找nearest neighborsman_idx = word_to_idx["man"]king_idx = word_to_idx["king"]woman_idx = word_to_idx["woman"]embedding = embedding_weights[woman_idx] - embedding_weights[man_idx] + embedding_weights[king_idx]cos_dis = np.array([scipy.spatial.distance.cosine(e, embedding) for e in embedding_weights])for i in cos_dis.argsort()[:20]: print(idx_to_word[i])
    0  留言 2019-06-23 14:57:53
  • 机器学习 22 、决策树 、模型融合

    前文链接:https://write-bug.com/article/2666.html
    决策树决策树(decision tree)是一种基本的分类与回归方法,模型:树结构
    主要有两大类算法:ID3 、C4.5
    学过数据结构的都知道,一棵树由节点node和有向边directed edge 构成,而节点又有几个名词:根节点、父节点、子节点、叶子节点
    这里我们用各个节点表示特征(如年龄),边表示特征属性(如青年、老年),叶子节点表示label(如买、不买)
    用决策树分类时,来了一个人根据他的特征,对某一特征进行比对,将其分到对应的子节点继续比对,直到到达叶子节点,得到预测label

    特征维度:年龄、收入、信誉、学生
    现在假设每个人都有这四类特征的数据,如何用一颗树来对这些人做一个是否购买的分类,也就是落地到某个叶子节点,那用哪个特征来做根节点父节点子节点呢?有很多组合方式。
    首先处理训练数据:

    数据特征值离散化:0 1 2 ……编码

    我们在进行特征选择时,要选择对训练数据具有分类能力的特征,以提高决策树的学习效率,决定了用哪个特征来划分特征空间,如果利用一个特征进行分类的结果和随即分类的结果没有差别,那么这个特征没有分类能力,而特征选择的准则就是信息增益与信息增益率,从而引出了ID3与C4.5算法
    ID3ID3的特征选择就是利用信息增益来进行选择,这也是我们前面提到的到底用哪个特征作为根节点哪个特征作为子节点构建树呢?
    说到信息增益,这里介绍几个概念:
    熵=随机变量的不确定性
    条件熵=在一定条件下,随机变量的不确定性
    信息增益=熵-条件熵 在一定条件下,信息不确定性减少的程度

    我们通常使用H(X)符号代表随机变量X的熵:
    H(X)=E(I(X))E代表期望值函数,I(X)代表随机变量X的自信息量。

    不确定性函数I称为事件的信息量,事件X发生概率p的单调递减函数:
    𝐼(X) = 𝑙𝑜𝑔(1/ 𝑝)=−𝑙𝑜𝑔 𝑝
    信息熵:一个信源中,不能仅考虑单一事件发生的不确定性,需要考虑所有可能情况的平均不确定性,为−𝑙𝑜𝑔 𝑝 的统计平均值E
    𝐻 (X) = 𝐸 [−𝑙𝑜𝑔 (𝑝 (x𝑖))] = −∑𝑖 𝑝(x𝑖 )𝑙𝑜𝑔 (𝑝 (x𝑖))这里的熵可以理解为混乱度,数据混乱度越大,熵取值越大,随机变量的不确定性越大
    如果H=0或1,说明数据为纯净的,一定为买或不买,而H=0.5时,混乱度最大

    伯努利分布时熵与概率的关系
    选择特征做父节点:选择熵最大(混乱程度最大)的特征
    例:

    ID3 完全用熵的概念:信息增益
    原始数据,无条件下的熵:

    label=买+不买=1024个p买=640/1024=0.635p不买 = 384/1024=0.375根据信息熵公式
    H=-∑p log(p)H=0.9544那么在年龄条件下的条件熵:
    P青年=384/1024=0.375青年分为买和不买:
    p买=128/384p不买=256/384S=384H=0.9183同理:中年:由于都是买的,所以H=0老年:H=0.9157
    年龄的三个信息熵得到了,那如何使用呢,我们对年龄求平均期望:
    平均值=占比\*信息熵E(年龄)=0.375*0.9183+0.25*0+0.375*0.9157=0.6877这个年龄作为区分的话,可以带来0.6877的增益G(年龄)=0.9544-0.6877=0.2667 为信息增益同理

    E(学生)=0.7811 G(学生)=0.1733
    E(收入)=0.9361 G(收入)=0.0183
    E(信誉)=0.9048 G(信誉)=0.0496

    从上述可以看出,按照年龄划分,收益是最大的,信息增益最大,所以年龄作为根节点,同理,从而构建整颗树.
    所以我们构建一个attrList特征列表作为一个容器存放特征,无放回的取特征
    如果取完了特征,构建出了树,但是叶子节点还是不是纯净的怎么办呢?
    分类问题:少数服从多数
    回归问题:概率比较

    缺点:因为优先选择信息增益最大的,所以导致倾向于选择属性值较多的
    贪婪性:选择收益最大的

    贪婪不是最优的算法,可能觉得本次收益最大,但从长远看收益没那么大
    奥卡姆剃刀原理:尽量用最少的东西做最多的事
    用最少的特征,区分类别,不需要所有特征都用,用最简单的方式效果最好,最优的树
    不适用于连续变量:离散型特征属性如果是身高这种线性连续的特征,对应过多类别特征,应离散化使用
    Pseudocode:
    ID3 (Examples, Target_Attribute, Attributes) Create a root node for the tree If all examples are positive, Return the single-node tree Root, with label = +. If all examples are negative, Return the single-node tree Root, with label = -. If number of predicting attributes is empty, then Return the single node tree Root, with label = most common value of the target attribute in the examples. Otherwise Begin A ← The Attribute that best classifies examples. Decision Tree attribute for Root = A. For each possible value, vi, of A, Add a new tree branch below Root, corresponding to the test A = vi. Let Examples(vi) be the subset of examples that have the value vi for A If Examples(vi) is empty Then below this new branch add a leaf node with label = most common target value in the examples Else below this new branch add the subtree ID3 (Examples(vi), Target_Attribute, Attributes – {A}) End Return RootID3实践:

    class ID3DTree(object): def __init__(self): self.tree={} self.dataSet=[] self.labels=[] def loadDataSet(self,path,labels): recordlist = [] fp = open(path,"r") # 读取文件内容 content = fp.read() fp.close() rowlist = content.splitlines() # 按行转换为一维表 recordlist=[row.split("\t") for row in rowlist if row.strip()] self.dataSet = recordlist self.labels = labels def train(self): labels = copy.deepcopy(self.labels) self.tree = self.buildTree(self.dataSet,labels) # 创建决策树主程序 def buildTree(self,dataSet,labels): cateList = [data[-1] for data in dataSet] # 抽取源数据集的决策标签列 # 程序终止条件1 : 如果classList只有一种决策标签,停止划分,返回这个决策标签 if cateList.count(cateList[0]) == len(cateList): return cateList[0] # 程序终止条件2: 如果数据集的第一个决策标签只有一个 返回这个决策标签 if len(dataSet[0]) == 1: return self.maxCate(cateList) # 算法核心: bestFeat = self.getBestFeat(dataSet) # 返回数据集的最优特征轴: bestFeatLabel = labels[bestFeat] tree = {bestFeatLabel:{}} del(labels[bestFeat]) # 抽取最优特征轴的列向量 uniqueVals = set([data[bestFeat] for data in dataSet]) # 去重 for value in uniqueVals: subLabels = labels[:] #将删除后的特征类别集建立子类别集 splitDataset = self.splitDataSet(dataSet, bestFeat, value) # 按最优特征列和值分割数据集 subTree = self.buildTree(splitDataset,subLabels) # 构建子树 tree[bestFeatLabel][value] = subTree return tree def maxCate(self,catelist): # 计算出现最多的类别标签 items = dict([(catelist.count(i), i) for i in catelist]) return items[max(items.keys())] def getBestFeat(self,dataSet): # 计算特征向量维,其中最后一列用于类别标签,因此要减去 numFeatures = len(dataSet[0]) - 1 # 特征向量维数 = 行向量维度-1 baseEntropy = self.computeEntropy(dataSet) # 基础熵:源数据的香农熵 bestInfoGain = 0.0; # 初始化最优的信息增益 bestFeature = -1 # 初始化最优的特征轴 # 外循环:遍历数据集各列,计算最优特征轴 # i 为数据集列索引:取值范围 0~(numFeatures-1) for i in range(numFeatures): # 抽取第i列的列向量 uniqueVals = set([data[i] for data in dataSet]) # 去重:该列的唯一值集 newEntropy = 0.0 # 初始化该列的香农熵 for value in uniqueVals: # 内循环:按列和唯一值计算香农熵 subDataSet = self.splitDataSet(dataSet, i, value) # 按选定列i和唯一值分隔数据集 prob = len(subDataSet)/float(len(dataSet)) newEntropy += prob * self.computeEntropy(subDataSet) infoGain = baseEntropy - newEntropy # 计算最大增益 if (infoGain > bestInfoGain): # 如果信息增益>0; bestInfoGain = infoGain # 用当前信息增益值替代之前的最优增益值 bestFeature = i # 重置最优特征为当前列 return bestFeature def computeEntropy(self,dataSet): # 计算香农熵 datalen = float(len(dataSet)) cateList = [data[-1] for data in dataSet] # 从数据集中得到类别标签 items = dict([(i,cateList.count(i)) for i in cateList]) # 得到类别为key,出现次数value的字典 infoEntropy = 0.0 # 初始化香农熵 for key in items: # 计算香农熵 prob = float(items[key])/datalen infoEntropy -= prob * math.log(prob,2) # 香农熵:= - p*log2(p) --infoEntropy = -prob * log(prob,2) return infoEntropy # 分隔数据集:删除特征轴所在的数据列,返回剩余的数据集 # dataSet:数据集; axis:特征轴; value:特征轴的取值 def splitDataSet(self, dataSet, axis, value): rtnList = [] for featVec in dataSet: if featVec[axis] == value: rFeatVec = featVec[:axis] # list操作 提取0~(axis-1)的元素 rFeatVec.extend(featVec[axis+1:]) # list操作 将特征轴(列)之后的元素加回 rtnList.append(rFeatVec) return rtnList def predict(self,inputTree,featLabels,testVec): # 分类器 root = list(inputTree.keys())[0] # 树根节点 secondDict = inputTree[root] # value-子树结构或分类标签 featIndex = featLabels.index(root) # 根节点在分类标签集中的位置 key = testVec[featIndex] # 测试集数组取值 valueOfFeat = secondDict[key] # if isinstance(valueOfFeat, dict): classLabel = self.predict(valueOfFeat, featLabels, testVec) # 递归分类 else: classLabel = valueOfFeat return classLabel # 存储树到文件 def storeTree(self,inputTree,filename): fw = open(filename,'w') pickle.dump(inputTree,fw) fw.close() # 从文件抓取树 def grabTree(self,filename): fr = open(filename) return pickle.load(fr)C4.5C4.5 用的是信息增益率的概念
    由上面,我们的信息增益:
    𝐺𝑎𝑖𝑛 (𝑆,𝐴)= 𝐸𝑛𝑡𝑟𝑜𝑝𝑦(𝑆) − ∑𝑣∈𝑉𝑎𝑙𝑢𝑒(𝐴) |𝑆𝑣|/ |𝑆| 𝐸𝑛𝑡𝑟𝑜𝑝𝑦 (𝑆𝑣)信息增益率:
    𝐺𝑎𝑖𝑛𝑅𝑎𝑡𝑖𝑜 𝐴 =𝐺𝑎𝑖𝑛(𝐴) / 𝐸𝑛𝑡𝑟𝑜𝑝𝑦(𝐴)比原来多除了一个entropy(A):属性背后的信息熵
    比如:前面的信息增益G(年龄)=0.9544-0.6877=0.2667
    𝐺𝑎𝑖𝑛𝑅𝑎𝑡𝑖𝑜 𝐴=0.2667/0.6877除以一个信息熵有什么好处呢?
    如果只考虑信息增益的话,我们没有考虑A集合背后的大小,A越大也就意味着集合越混乱的可能性越高,那么C4.5考虑到了这种情况,给集合的混乱度做一个中和
    C4.5实践:


    class C45DTree(object): def __init__(self): self.tree={} self.dataSet=[] self.labels=[] def loadDataSet(self,path,labels): recordlist = [] fp = open(path,"r") content = fp.read() fp.close() rowlist = content.splitlines() recordlist=[row.split("\t") for row in rowlist if row.strip()] self.dataSet = recordlist self.labels = labels def train(self): labels = copy.deepcopy(self.labels) self.tree = self.buildTree(self.dataSet,labels) def buildTree(self,dataSet,labels): cateList = [data[-1] for data in dataSet] if cateList.count(cateList[0]) == len(cateList): return cateList[0] if len(dataSet[0]) == 1: return self.maxCate(cateList) bestFeat, featValueList = self.getBestFeat(dataSet) bestFeatLabel = labels[bestFeat] tree = {bestFeatLabel:{}} del(labels[bestFeat]) for value in featValueList: subLabels = labels[:] splitDataset = self.splitDataSet(dataSet, bestFeat, value) subTree = self.buildTree(splitDataset,subLabels) tree[bestFeatLabel][value] = subTree return tree def maxCate(self,catelist): items = dict([(catelist.count(i), i) for i in catelist]) return items[max(items.keys())] def getBestFeat(self, dataSet): Num_Feats = len(dataSet[0][:-1]) totality = len(dataSet) BaseEntropy = self.computeEntropy(dataSet) ConditionEntropy = [] # 初始化条件熵 splitInfo = [] # for C4.5, calculate gain ratio allFeatVList=[] for f in range(Num_Feats): featList = [example[f] for example in dataSet] [splitI,featureValueList] = self.computeSplitInfo(featList) allFeatVList.append(featureValueList) splitInfo.append(splitI) resultGain = 0.0 for value in featureValueList: subSet = self.splitDataSet(dataSet, f, value) appearNum = float(len(subSet)) subEntropy = self.computeEntropy(subSet) resultGain += (appearNum/totality)*subEntropy ConditionEntropy.append(resultGain) # 总条件熵 infoGainArray = BaseEntropy*ones(Num_Feats)-array(ConditionEntropy) infoGainRatio = infoGainArray/array(splitInfo) # c4.5, info gain ratio bestFeatureIndex = argsort(-infoGainRatio)[0] return bestFeatureIndex, allFeatVList[bestFeatureIndex] def computeSplitInfo(self, featureVList): numEntries = len(featureVList) featureVauleSetList = list(set(featureVList)) valueCounts = [featureVList.count(featVec) for featVec in featureVauleSetList] # caclulate shannonEnt pList = [float(item)/numEntries for item in valueCounts ] lList = [item*math.log(item,2) for item in pList] splitInfo = -sum(lList) return splitInfo, featureVauleSetList def computeEntropy(self,dataSet): datalen = float(len(dataSet)) cateList = [data[-1] for data in dataSet] items = dict([(i,cateList.count(i)) for i in cateList]) infoEntropy = 0.0 for key in items: prob = float(items[key])/datalen infoEntropy -= prob * math.log(prob,2) return infoEntropy def splitDataSet(self, dataSet, axis, value): rtnList = [] for featVec in dataSet: if featVec[axis] == value: rFeatVec = featVec[:axis] rFeatVec.extend(featVec[axis+1:]) rtnList.append(rFeatVec) return rtnList # 树的后剪枝 # testData: 测试集 def prune(tree, testData): pass def predict(self,inputTree,featLabels,testVec): root = list(inputTree.keys())[0] secondDict = inputTree[root] featIndex = featLabels.index(root) key = testVec[featIndex] valueOfFeat = secondDict[key] # if isinstance(valueOfFeat, dict): classLabel = self.predict(valueOfFeat, featLabels, testVec) else: classLabel = valueOfFeat return classLabel def storeTree(self,inputTree,filename): fw = open(filename,'w') pickle.dump(inputTree,fw) fw.close() def grabTree(self,filename): fr = open(filename) return pickle.load(fr)之前的ID3和C4.5停止条件都是结点数据类型一致(即纯净),要不就是没有特征可以选择了,但是只有这两个条件限制,在特征多树学习过深的情况下,模型容易过拟合
    防止过拟合:

    停止条件:信息增益(比例)增长低于阈值,则停止
    – 阈值需要调校 将数据分为训练集和测试集
    – 测试集上错误率增长,则停止
    剪枝:

    相邻的叶子节点,如果合并后不纯度增加在允许范围内,则合并
    测试集错误率下降,则合并 等等

    sklearn- tree实践:
    from sklearn.tree import DecisionTreeRegressor# Fit regression modelregr_2 = DecisionTreeRegressor(max_depth=10)#深度阈值print >> sys.stderr, 'begin train ==============='regr_2.fit(X, y)print >> sys.stderr, 'begin predict ==============='# Predicty_2 = regr_2.predict(X_test)for p_label in y_2: print p_label #输出预测标签(回归值)print >> sys.stderr, 'end ==============='多分类用MSE做评估:
    import sysfrom sklearn import metricsimport numpy as npimport randomres_file = sys.argv[1]y_test = []y_pred = []with open(res_file, 'r') as fd: for line in fd: ss = line.strip().split('\t') if len(ss) != 2: continue t_label = float(ss[0].strip()) p_label = float(ss[1].strip()) y_test.append(t_label) y_pred.append(p_label)print "MSE:",metrics.mean_squared_error(y_test, y_pred)print "RMSE:",np.sqrt(metrics.mean_squared_error(y_test, y_pred))模型融合Ensemble Learning(集成学习)什么是集成学习呢?假设我有多个模型,每个模型的学习准确率都是80%,或者说每个模型的准确率有学习好的有学习坏的,这时就有很多数据没有预估正确,那么我使用多个模型来共同决策,少数服从多数,通过这种方式提升准确率,把多个弱分类器集成获得一个强分类器。
    集成学习里面有两类算法:Bagging和Boosting算法
    Bagging算法是以random forest随机森林为代表的算法
    Boosting算法是以AdaBoost算法和Gbdt为代表的算法
    通过介绍我们知道这是将多个模型融合,得到一个强模型的方法,那么我们是如何证明这种方式是否靠谱呢?
    用数学上的排列组合问题表示,假设二分类问题有5个精确度为70%的弱分类器,相互独立
    采用投票的方式将5个分类器的结果进行集成:
    𝐶5 3(0.73)(0.32)+ 𝐶5 4(0.74)(0.31)+ 𝐶5 5(0.71) = 10(.7^3)(.3^2)+5(.7^4)(.3)+(.7^5) = 83.7%如果有101个分类器,精确度就可以提升到99.9%

    问题:如何得到不同的分类器,并且尽量独立? (每个模型不尽相同,不相互影响)
    方法:将一个复杂的学习问题,分解成多个简单的学习问题

    Bagging– 对训练数据采样,得到多个训练集,分别训练模型
    – 对得到的多个模型进行平均 分类问题:投票方式进行集成 回归问题:模型的输出进行平均

    训练:同一训练数据随机抽取数据并行用不同的机器和模型同时训练
    测试:对模型预测出的各个分数进行投票或者平均
    适合弱分类器(容易过拟合,效果一般):
    不稳定:随机采样会得到较不同的分类器,每个基分类器准确率略高于50%
    例如:决策树
    不适合强分类器(本身准确率高,效果较好) :
    稳定:随机采样对结果影响不大
    甚至可能不如不集成,每个基分类器只有更少的训练样本
    例如:DNN(模型复杂时,时间成本大并且容易过拟合,为了抗过拟合,最有效的办法就是让训练数据越大越好,和bagging的选取数据思想不同)
    Random Forest
    Random Forest = Bagging+决策树
    随机森林生成方法:
    – 从样本集中通过重采样的方式产生n个样本
    – 建设样本特征数目为a,对n个样本选择a中的k个特征,用建立决策树的方式获得最佳分割点
    – 重复m次,产生m棵决策树
    – 多数投票机制进行预测
    每棵树样本不同,选择特征可能不同,形成树的形状、深度都不同。
    优点:

    训练速度快、容易实现并行
    训练完成后,反馈哪些是重要特征(类似LR中的特征权重,这里的决策树靠信息熵筛选)
    抗过拟合能力强 (多棵树抗过拟合)
    可解决分类、回归问题
    不平衡数据集,rf是平衡误差有效方法

    缺点:

    取值较多的属性,影响大
    噪音大的分类、回归问题会过拟合
    只能尝试参数和种子的选择,无法洞悉内部

    Boosting每个基分类器,基于之前的分类结果,已经分对的样本不用太多关心,集中力量对付分错样本,不等权投票,好的基分类器权重大
    – 对训练数据集进行采样,对错分样本加权采样
    – 多个模型按照分类效果加权融合

    训练:同一训练数据随机抽取数据串行用不同的机器和模型进行训练,首先模型一学习预估评分后,对出错的样本,提高采样权重,用模型二再次随机抽取数据,这时由于出错样本采样权重提高重点会放在出错的样本上,以此类推串行迭代下去
    测试:对各个模型预测出的各个分数乘以对应的错误率权重公式相加
    Boosting方法的两类思想:
    传统Boost→ Adaboost:对正确、错分样本加权,每步的迭代,错误样本加权,正确样本降权
    Gradient Boosting:每次建模建立在之前模型损失函数的梯度下降方向
    Adaboost
    对不同的模型分数加权求和,错误样本加权,正确样本降权


    给数据中的每一个样本一个权重
    训练数据中的每一个样本,得到第一个分类器
    计算该分类器的错误率,根据错误率计算要给分类器分配的权重(注意这里是分类器的权重) 即前面表示的每个模型预估后的分数乘对应的分类器权重求和

    错误率:𝜀 =未正确分类的样本数目/ 所有样本数目 分类器权重: 𝛼 =1/ 2 ln((1 – 𝜀)/ 𝜀)

    将第一个分类器分错误的样本权重增加,分对的样本权重减小(注意这里是样本的权重)
    错分样本权重:𝐷𝑖^ (𝑡+1) = 𝐷𝑖 ^(𝑡)𝑒^𝑎 /𝑆𝑢𝑚(𝐷),Di^t表示上一次样本的权重,分母:训练集的规模,样本个数
    正确样本权重:𝐷𝑖^ (𝑡+1) = 𝐷𝑖^ (𝑡)𝑒^(−𝑎) / 𝑆𝑢𝑚(𝐷)

    然后再用新的样本权重训练数据,得到新的分类器,到步骤3
    直到步骤3中分类器错误率为0,或者到达迭代次数
    将所有弱分类器加权求和,得到分类结果(注意是分类器权重)

    GBDT(Gradient Boosting Decision Tree)
    是一种迭代的决策树算法,该算法由多棵决策树组,所有树的结论累加起来做最终答案
    三个概念组成:
    – Regression Decistion Tree(DT、RT)
    – Gradient Boosting(GB)
    – Shrinkage(步长)
    分类树:衡量标准最大熵——预测分类标签值
    回归树:衡量标准是最小化均方差——预测实际数值
    GBDT中的树,属于回归树,不是分类树 – 要求所有树的结果进行累加是有意义的 – GBDT是结果累加,而不是多数投票
    GBDT核心: – 每一棵树学的是之前所有树结论和的残差 – 残差就是一个加预测值后能得真实值的累加量
    残差是什么?

    因为是回归树,那么我的节点值应该是这个节点的平均值即预估值,那么来了一个人落在左边这个节点就是15岁,但是对于某些人预估的大了,有的预估的小了,那我把残差整理成新的样本再建一棵树重新学习,尽量把残差学没了,就像以前的误差值,尽力把它学为0
    这里同样的,特征选择时也是利用信息熵的方式。
    残差是学习的最优方向,体现了Gradient
    Gradient Descend梯度下降实际上是在更新参数,并且最终参数等于每次迭代的增量的累加和:
    𝜃𝑡 = 𝜃𝑡−1 + 𝜃𝑡𝜃𝑡 = −𝑎𝑡𝑔𝑡 参数更新方向为负梯度方向𝜃 =∑𝜃𝑡 最终参数等于每次迭代的增量的累加和, 𝜃0为初值而Gradient Boost是在更新整个函数
    𝑓𝑡 𝑥 = 𝑓𝑡−1 𝑥 + 𝑓𝑡 (𝑥)𝑓𝑡(𝑥) = −𝑎𝑡𝑔𝑡(𝑥) 类似地,拟合了负梯度𝐹(𝑥) = ∑𝑓𝑡(𝑥) 最终参数等于每次迭代的增量的累加和 𝑓0(𝑥)为模型初值,通常为函数所以,Boosting是一种加法模型GBDT模型F定义为加法模型
    𝐹 (𝑥;𝑤) = ∑𝑎𝑡 ℎ𝑡(𝑥;𝑤𝑡)= 𝑓𝑡(𝑥;𝑤𝑡)x为输入样本,h为分类回归树,w是分类回归树的参数,a是每棵树的权重

    2.1的公式是loss值更新函数预估值,2.2是求残差
    但是这样的加法模式比较容易过拟合,这就涉及到了Shrinkage缩减技术:
    思想:每棵树只学到了真理的一小部分,累加的时候只累加一小部分,通过多学几棵树弥补不足
    假设: yi表示第i棵树上y的预测值, y(1~i)表示前i棵树y的综合预测值
    没有shrinkage时:
    – y(i+1) = 残差(y1~yi), 其中: 残差(y1~yi) = y真实值 - y(1 ~ i)
    – y(1 ~ i) = SUM(y1, …, yi)
    有shrinkage时:
    – 第2个方程调整为:y(1 ~ i) = y(1 ~ i-1) + step /* yi
    即对每次的结果都乘一个权重
    仍然以残差作为学习目标,但对于残差的学习,每次只累加一小部分(step残差)逐步逼近目标
    本质上,Shrinkage为每棵树设置了一个weight,累加时要乘以这个weight
    偏差、方差
    偏差bias:模型不准确
    尽量选择正确模型与复杂度
    模型越简单,预估能力越差,偏差越大,方差越低
    方差variance:模型不稳定
    防止过拟合

    我们的模型融合是可以同时把方差与偏差同时降低的方法。
    RF实践:
    sklearn - rf:
    测试数据:

    import sysimport numpy as npimport matplotlib.pyplot as pltimport pandas as pd# Importing the datasetsdatasets = pd.read_csv('Position_Salaries.csv')#pandas自动解析读取X = datasets.iloc[:, 1:2].values #级别Y = datasets.iloc[:, 2].values #金额# Fitting the Regression model to the datasetfrom sklearn.ensemble import RandomForestRegressorregressor = RandomForestRegressor(n_estimators = 300, random_state = 0)#300颗树regressor.fit(X,Y)# Predicting a new result with the Random Forest RegressionY_Pred = regressor.predict([[6.5]])print(Y_Pred)# Visualising the Random Forest Regression results in higher resolution and smoother curveX_Grid = np.arange(min(X), max(X), 0.01)X_Grid = X_Grid.reshape((len(X_Grid), 1))plt.scatter(X,Y, color = 'red')plt.plot(X_Grid, regressor.predict(X_Grid), color = 'blue')plt.title('Random Forest Regression Results')plt.xlabel('Position level')plt.ylabel('Salary')plt.show()output:

    pyspark-mllib-rf:
    测试数据:

    #coding=utf8from __future__ import print_functionimport sysfrom pyspark import SparkContext, SparkConffrom pyspark.mllib.regression import LabeledPointfrom pyspark.mllib.feature import HashingTF,IDF,StandardScalerfrom pyspark.mllib.classification import LogisticRegressionWithSGD,SVMWithSGD,NaiveBayesfrom pyspark.mllib.tree import DecisionTreefrom pyspark.mllib.tree import RandomForest, RandomForestModelfrom pyspark.mllib.util import MLUtilsreload(sys)sys.setdefaultencoding('utf-8')if __name__ == "__main__": #conf = SparkConf().setMaster("spark://master:7077").setAppName("lr_pyspark_test") conf = SparkConf().setMaster("local").setAppName("lr_pyspark_test") sc = SparkContext(conf=conf) #in_file = sc.textFile("file:///root/spark_test_5/pyspark_test/lr_pyspark_test/data/a8a") data = MLUtils.loadLibSVMFile(sc, "file:///root/spark_test_5/pyspark_test/lr_pyspark_test/data/a8a") (trainingData, testData) = data.randomSplit([0.7, 0.3]) model = RandomForest.trainClassifier(\ trainingData, numClasses=2, \ categoricalFeaturesInfo={},\ numTrees=3, \ featureSubsetStrategy="auto",\ impurity='gini', maxDepth=4, maxBins=32)#3颗树,gini系数方式 predictions = model.predict(testData.map(lambda x: x.features)) labelsAndPredictions = testData.map(lambda lp: lp.label).zip(predictions) testErr = labelsAndPredictions.filter(lambda (v, p): v != p).count() / float(testData.count()) print('Test Error = ' + str(testErr)) print('Learned classification forest model:') print(model.toDebugString()) ## Save and load model #model.save(sc,"target/tmp/myRandomForestClassificationModel") #sameModel = RandomForestModel.load(sc, "target/tmp/myRandomForestClassificationModel") sc.stop()GBDT实践:
    微软lightgbm方式:
    import sysimport lightgbm as lgb #pip安装import numpy as npfrom scipy.sparse import csr_matriximport pandas as pdfrom sklearn.metrics import mean_squared_errorfrom sklearn.datasets import load_irisfrom sklearn.model_selection import train_test_splitfrom sklearn.datasets import make_classificationdata_in = 'D:\\bd\\share_folder\\a9a'def load_data(): target_list = [] fea_row_list = [] fea_col_list = [] data_list = [] row_idx = 0 max_col = 0 with open(data_in, 'r') as fd: for line in fd: ss = line.strip().split(' ') label = ss[0] fea = ss[1:] target_list.append(int(label)) for fea_score in fea: sss = fea_score.strip().split(':') if len(sss) != 2: continue feature, score = sss fea_row_list.append(row_idx) fea_col_list.append(int(feature)) data_list.append(float(score)) if int(feature) > max_col: max_col = int(feature) row_idx += 1 row = np.array(fea_row_list) col = np.array(fea_col_list) data = np.array(data_list) fea_datasets = csr_matrix((data, (row, col)), shape=(row_idx, max_col + 1)) x_train, x_test, y_train, y_test = train_test_split(fea_datasets, target_list, test_size=0.2, random_state=0) return x_train, x_test, y_train, y_testX_train,X_test,y_train,y_test = load_data()# 创建成lgb特征的数据集格式lgb_train = lgb.Dataset(X_train, y_train)lgb_eval = lgb.Dataset(X_test, y_test, reference=lgb_train)params = { 'task': 'train', 'boosting_type': 'gbdt', # 设置提升类型 'objective': 'regression', # 目标函数 'metric': {'l2', 'auc'}, # 评估函数 'num_leaves': 31, # 叶子节点数 'learning_rate': 0.05, # 学习速率 'feature_fraction': 0.9, # 建树的特征选择比例 'bagging_fraction': 0.8, # 建树的样本采样比例 'bagging_freq': 5, # k 意味着每 k 次迭代执行bagging 'verbose': 1 # <0 显示致命的, =0 显示错误 (警告), >0 显示信息}print('Start training...')# 训练 cv and traingbm = lgb.train(params,lgb_train,num_boost_round=20,valid_sets=lgb_eval,early_stopping_rounds=5)print('Save model...')# 保存模型到文件gbm.save_model('model.txt')print('Start predicting...')# 预测数据集y_pred = gbm.predict(X_test, num_iteration=gbm.best_iteration)print(y_test)print(y_pred)# 评估模型print('The rmse of prediction is:', mean_squared_error(y_test, y_pred) ** 0.5)部分模型output:
    0  留言 2019-06-21 08:22:26
  • 深度学习 21、RNN

    前文链接:https://write-bug.com/article/2644.html
    RNN循环神经网络
    RNN(循环神经网络):一类用于处理序列(时间、空间)数据的神经网络,RNN可用于分类、回归、机器翻译、文本相似度
    隐层的神经元不仅接收输入层的或者上一层的信息流,还要接受上一时刻的同层神经元的信息流,即隐层之间的连接,连接传递的权重就是记忆能力。
    此处的记忆能力理解为人的认知往往基于过往的经验与记忆,所以模拟时,同样根据激活函数的不同会产生梯度消失和梯度爆炸问题,也反映了人的记忆往往有限,过往太久的事不会记忆或者对现在产生影响过大。
    梯度消失:每次loss反向传播时,如果使用sigmoid这样的激活函数一直乘一个小数,在这个过程中一直变小,直到逼近于0梯度爆炸:每次loss反向传播时,如果使用relu这样的激活函数,x>0时,y=x,一直乘一个大于0的数,数值过大
    所以,他的优点就是在上下文推断时,可以根据上文推断下一个出现概率最大的单词,适合短期记忆,不适合长期记忆,很难学习长距离信息,那么同样的,比较符合大脑记忆曲线

    隐层展开后为上图样式。

    t-1, t, t+1表示时间序列
    X表示输入的样本
    St表示样本在时间t处的的记忆,St = f(WSt-1 +UXt)
    W表示输入的权重
    U表示此刻输入的样本的权重

    这里的W,U,V在每个时刻都是相等的(权重共享),隐藏状态: S=f(现有的输入+过去记忆总结)。

    以上,便是最基本的RNN原理,我们为了更好的模拟人们根据过往的经验来判断和写文章,引出了
    LSTM( Long Short Term Memory Network长短期记忆网络)
    LSTM结构更复杂,不仅适合短期记忆,也适合长期记忆。

    这条黑线就是从上一时刻的输入到此刻的输出,即单元状态流,用于记忆当前状态,乘号代表着当前时刻对记忆的削减,加号代表记忆的更新,也就是新信息的汇入。
    LSTM包含3个门 (– 遗忘门 – 输入门 – 输出门)这里有这个“门”的概念,下面一个一个解释:
    遗忘门

    上一刻的信息与此刻的新信息汇入后,σ层就会把信息转化为ft,如果决定要忘记,ft就是0,如果这 个信息要保留,则为1,选择部分记忆则按照实际情况输出0~1的数,也就是激活函数,如sigmoid

    ht-1是上个网络的output(输出)xt当前网络的input(输入)网络会自动学习到对应的权重矩阵
    输入门

    首先,上一刻的信息与此刻的新信息汇入后,,统一经过σ层,决定要更新啥信息。还要另外经过tanh层,将 信息转化为一个新的候选值,两者再相乘,就是cell state中需要更新的值了。


    接着,“遗忘门”和“输入门”已经决定好了 什么要遗忘,什么要更新

    输出门

    前面的门已经决定了信息的状态如何保留和更新,得到了Ct,现在把它经过tanh层,并再次与σ层识别哪一部分信息要输出的信息相乘就得到了输出信息ht,输出两路分别给输出(比如此刻的预测词)和下一时刻输入。
    GRULSTM的变体,比LSTM结构更加简单,效果也很好,GRU只有两个门:更新门、重置门


    zt和rt分别表示更新门和重置门

    更新门用于控制前一时刻的状态信息被带入到当前状态中的程度 ,更新门的值越大说明前一时刻的状态信息带入越多
    重置门控制前一状态有多少信息被写入到当前的候选集 h̃ t 上 ,重置门越小,前一状态的信息被写入的越少

    LSTM 实践# define modelclass RNNModel(nn.Module): """ 一个简单的循环神经网络""" def __init__(self, rnn_type, ntoken, ninp, nhid, nlayers, dropout=0.5): ''' 该模型包含以下几层: - 词嵌入层 - 一个循环神经网络层(RNN, LSTM, GRU) - 一个线性层,从hidden state到输出单词表 - 一个dropout层,用来做regularization ''' super(RNNModel, self).__init__() self.drop = nn.Dropout(dropout) self.encoder = nn.Embedding(ntoken, ninp) if rnn_type in ['LSTM', 'GRU']: self.rnn = getattr(nn, rnn_type)(ninp, nhid, nlayers, dropout=dropout) else: try: nonlinearity = {'RNN_TANH': 'tanh', 'RNN_RELU': 'relu'}[rnn_type] except KeyError: raise ValueError( """An invalid option for `--model` was supplied, options are ['LSTM', 'GRU', 'RNN_TANH' or 'RNN_RELU']""") self.rnn = nn.RNN(ninp, nhid, nlayers, nonlinearity=nonlinearity, dropout=dropout) self.decoder = nn.Linear(nhid, ntoken) self.init_weights() self.rnn_type = rnn_type self.nhid = nhid self.nlayers = nlayers def init_weights(self): initrange = 0.1 self.encoder.weight.data.uniform_(-initrange, initrange) self.decoder.bias.data.zero_() self.decoder.weight.data.uniform_(-initrange, initrange) def forward(self, input, hidden): ''' Forward pass: - word embedding - 输入循环神经网络 - 一个线性层从hidden state转化为输出单词表 ''' #print("input ", input.size()) emb = self.drop(self.encoder(input)) #print("emb ", emb.size()) output, hidden = self.rnn(emb, hidden) #print("output ", output.size()) #print(len(hidden)) output = self.drop(output) decoded = self.decoder(output.view(output.size(0)*output.size(1), output.size(2))) #print("output size: ", output.size()) #print("decoded before size: ", output.view(output.size(0)*output.size(1), output.size(2)).size()) #print("decoded after size: ", decoded.size()) #sys.exit() return decoded.view(output.size(0), output.size(1), decoded.size(1)), hidden def init_hidden(self, bsz, requires_grad=True): weight = next(self.parameters()) if self.rnn_type == 'LSTM': return (weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad), weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad)) else: return weight.new_zeros((self.nlayers, bsz, self.nhid), requires_grad=requires_grad)model = RNNModel("LSTM", VOCAB_SIZE, EMBEDDING_SIZE, EMBEDDING_SIZE, 2, dropout=0.5)#模型初始化if USE_CUDA: model = model.cuda()
    0  留言 2019-06-16 20:07:46
  • 深度学习 20、CNN

    前文链接:https://write-bug.com/article/2575.html
    卷积神经网络CNN上节我们介绍了DNN的基本网络结构和演示,其网络结构为:

    DNN BP神经网络实行全连接特性,参数权值过多,需求大量样本,计算困难,所以CNN利用局部连接、权值共享来实现减少权值的尝试,即局部感知野和权值共享。擅长图像分类问题,识别二维图形,现在也可以用于文本处理。

    CNN的隐藏层分为卷积层conv、和池化层pool,一般CNN结构(LeNet)依次为:

    INPUT->[[CONV -> RELU]N -> POOL?]M -> [FC -> RELU]*K->FC经典LeNet-5结构:最早用于数字识别的CNN


    卷积层Convolitions:
    用卷积层实现对图片局部的特征提取,窗口模式卷积核形式提取特征,映射到高维平面。
    两个关键性操作:

    局部关联:每个神经元看为一个滤波器filter
    窗口滑动:filter对局部数据计算

    局部感受野:
    1000 * 1000 图像 1000 * 1000神经元全连接: 1000 * 1000 * 1000 * 1000 = 10^12 个参数
    局部感受野:每个神经元只需要知道10 * 10区域,训练参数降到100M

    权值共享:
    所有神经元用同一套权重值,100M降到100,这样得到一个feature map。100种权重值100个map,一共需要训练100*100 = 10k参数

    卷积的计算:

    窗口滑动,我们可以想象一个窗口(卷积核feature_map),设置固定的feature_size,里面是固定的权值,这个窗口在同一张图片上从左到右从上到下游走,每次固定步长,从而得到更小size的矩阵。

    这里有两个神经元的卷积层,步长stride设置为2,边缘灰色部分为填充值zero-padding,防止窗口移到边缘时不够滑动遍历所有像素,而这里的神经元个数就是卷积层深度depth。
    在卷积层中每个神经元连接数据窗的权重是固定的,每个神经元只关注一个特性。神经元就是图像处理中的滤波器,比如边缘检测专用的Sobel滤波器,即卷积层的每个滤波器都会有自己所关注一个图像特征,比如垂直边缘,水平边缘,颜色,纹理等等,这些所有神经元加起来就好比就是整张图像的特征提取器集合。

    需要估算的权重个数减少: AlexNet 1亿 => 3.5w
    一组固定的权重和不同窗口内数据做内积: 卷积卷积理解:后面输出结果的前提是依赖前面相应权值积累,比如连续吃三天药身体才好,那么这三天的相关性是不同的权值

    激活函数把卷积层输出结果做非线性映射。CNN采用的激励函数一般为ReLU(The Rectified Linear Unit/修正线性单元),它的特点是收敛快,求梯度简单,但较脆弱,图像如下。

    激励层的实践经验:

    不要用sigmoid!不要用sigmoid!不要用sigmoid!首先试RELU,因为快,但要小心点如果2失效,请用Leaky ReLU或者Maxout某些情况下tanh倒是有不错的结果,但是很少
    池化层pooling:对特征进一步缩放
    池化层夹在连续的卷积层中间,用于压缩数据和参数的量,减小过拟合,压缩图像。
    特点:

    特征不变性:图像缩小还可以认出大概
    特征降维:去除冗余信息,防止过拟合


    池化层用的方法有Max pooling 和 average pooling,而实际用的较多的是Max pooling。
    这里就说一下Max pooling,其实思想非常简单。

    对于每个2*2的窗口选出最大的数作为输出矩阵的相应元素的值,比如输入矩阵第一个2*2窗口中最大的数是6,那么输出矩阵的第一个元素就是6,如此类推。
    平移不变性

    旋转不变性

    缩放不变性

    全连接层FC
    和BP神经网络一样,两层之间互相全连接,即把压缩后的维度打平为一组向量,再通过dnn的方法进行分类处理。
    Droupout
    正则化方法,CNN中解决过拟合问题,同时通用与其他网络。

    随机在全连接层,剪掉一些神经元,防止过拟合。
    卷积网络在本质上是一种输入到输出的映射,它能够学习大量的输入与输出之间的映射关系,而不需要任何输入和输出之间的精确的数学表达式,只要用已知的模式对卷积网络加以训练,网络就具有输入输出对之间的映射能力。
    CNN一个非常重要的特点就是头重脚轻(越往输入权值越小,越往输出权值越多),呈现出一个倒三角的形态,这就很好地避免了BP神经网络中反向传播的时候梯度损失得太快。
    卷积神经网络CNN主要用来识别位移、缩放及其他形式扭曲不变性的二维图形。由于CNN的特征检测层通过训练数据进行学习,所以在使用CNN时,避免了显式的特征抽取,而隐式地从训练数据中进行学习;再者由于同一特征映射面上的神经元权值相同,所以网络可以并行学习,这也是卷积网络相对于神经元彼此相连网络的一大优势。卷积神经网络以其局部权值共享的特殊结构在语音识别和图像处理方面有着独特的优越性,其布局更接近于实际的生物神经网络,权值共享降低了网络的复杂性,特别是多维输入向量的图像可以直接输入网络这一特点避免了特征提取和分类过程中数据重建的复杂度。
    卷积神经网络之训练算法:

    同一般机器学习算法,先定义Loss function,衡量和实际结果之间差距
    找到最小化损失函数的W和b, CNN中用的算法是SGD(随机梯度下降)

    卷积神经网络之优缺:

    优点

    共享卷积核,对高维数据处理无压力无需手动选取特征,训练好权重,即得特征分类效果好
    缺点

    需要调参,需要大样本量,训练最好要GPU物理含义不明确(也就说,我们并不知道没个卷积层到底提取到的是什么特征,而且神经网络本身就是一种难以解释的“黑箱模型”)

    卷积神经网络之典型CNN:

    LeNet,这是最早用于数字识别的CNN
    AlexNet, 2012 ILSVRC比赛远超第2名的CNN,比LeNet更深,用多层小卷积层叠加替换单大卷积层。
    ZF Net, 2013 ILSVRC比赛冠军
    GoogLeNet, 2014 ILSVRC比赛冠军
    VGGNet, 2014 ILSVRC比赛中的模型,图像识别略差于GoogLeNet,但是在很多图像转化学习问题(比如object detection)上效果奇好pytorch网络结构:

    class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 20, 5, 1) self.conv2 = nn.Conv2d(20, 50, 5, 1) self.fc1 = nn.Linear(4*4*50, 500) self.fc2 = nn.Linear(500, 10) def forward(self, x): print("x size: ", x.size()) x = F.relu(self.conv1(x)) print("conv1 size: ", x.size()) x = F.max_pool2d(x, 2, 2) print("pool size: ", x.size()) x = F.relu(self.conv2(x)) print("conv2 size: ", x.size()) x = F.max_pool2d(x, 2, 2) print("pool size: ", x.size()) x = x.view(-1, 4*4*50) print("view size: ", x.size()) x = F.relu(self.fc1(x)) print("fc1 size: ", x.size()) x = self.fc2(x) print("fc2 size: ", x.size()) sys.exit() return F.log_softmax(x, dim=1)
    0  留言 2019-06-11 11:42:43
  • 深度学习 19、DNN

    前文链接:https://write-bug.com/article/2540.html
    深度学习在前几节介绍的模型都属于单点学习模型,比如朴素贝叶斯的模型为概率,lr与softmax的模型为w,b权重,聚类kmeans的模型为中心向量点,还有后面的决策树的树模型等等都属于单点学习模型,都有各自的用途,而这里的深度学习来说,属于机器学习的一部分,可用不同的网络结构来解决各个领域的问题,模型为深度学习网络。
    通过前几节的学习,不知道大家有没有对单点学习的模型有感触,所谓模型是什么?
    我们从已有的例子(训练集)中发现输入x与输出y的关系,这个过程是学习(即通过有限的例子发现输入与输出之间的关系),而我们使用的function就是我们的模型,通过模型预测我们从未见过的未知信息得到输出y,通过激活函数(常见:relu,sigmoid,tanh,swish等)对输出y做非线性变换,压缩值域,而输出y与真实label的差距就是误差error,通过损失函数再求取参数偏导,梯度下降降低误差,从而优化模型,我们前面见过的损失函数是mse和交叉熵

    而DNN的模型就是网络结构,在说如何选取更好的网络结构之前,我们先介绍下神经网络:

    人类大脑的神经网络如上图具象出来就是无数个神经元,而神经元分为几个结构:

    树突:接收信号源
    神经元:核心计算单元
    轴突:传输数据到下一个神经元

    通过一个个神经元的传输,生成结果,比如说条件反射:缩手
    而在我们模型中,这个网络怎么表示呢?

    感知机:最最简单的神经网络,只有一个神经元


    树突:样本特征a1
    轴突:权重w1

    树突与轴突,也就是样本特征和权重相乘,有些类似lr的wx,共同决策了神经元的输入

    神经元:对信号进一步处理,比如lr中wx的∑,并加入偏置b,默认为1
    传递函数(激活函数):对f(s)做压缩非线性变换

    假设一个二分类问题:

    输入:
    树突P1:颜色
    树突P2:形状
    香蕉(黄色,弯形):(-1,-1)
    苹果(红色,圆形):(1,1)

    相当于坐标轴的两个点,我们需要一个分类面把两个点分割开。

    P1颜色的取值为1,-1
    P2形状的取值为1,-1
    初始化w1=w2=1,b=0

    则有:
    s=p1w1+p2w2=2>0s=p2 2w2 2+p2 2w2 2=-2 < 0这个初始化的参数可以区分,但是初始化有随机性,换一组参数就不能区分了
    所以感知机的训练方法,目的就是训练权重和偏置
    w=w+epb=b+ee(误差)=t-a=期望-实际
    如,假设w1=1,w2=-1,b=0

    苹果s=0
    期望为1,e=1-0=1

    修正:
    w1=1+1*1=2w2=-1+1*1=0b=0+1=1s=3>0为苹果但是,y=wx+b只能解决线性可分的问题,对于异或问题,也就是四点对立面,需要一个曲线做分割,而这就意味着需要多层神经元和信号源共同决策分割面,也就形成了复杂的神经网络,而目标:同样是那么多边的权重。

    通过不同层间的结果作为下一层的输入,再对输入中间结果进一步处理,以此类推形成足够的深度,对于大部分问题来说都可以提到一个提升效果的作用。
    感知机只有从输出层具有激活函数,即功能神经元,这里的隐层和输出层都是具有激活函数的功能神经元。
    层与层全连接,同层神经元不连接,不跨层连接的网络又称为多层前馈神经网络。
    前馈:网络拓扑结构上不存在环和回路
    我们通过pytorch实现演示:
    二分类问题:
    假数据准备:
    # make fake data#正态分布随机产生n_data = torch.ones(100, 2)x0 = torch.normal(2*n_data, 1) # class0 x data (tensor), shape=(100, 2)y0 = torch.zeros(100) # class0 y data (tensor), shape=(100, 1)x1 = torch.normal(-2*n_data, 1) # class1 x data (tensor), shape=(100, 2)y1 = torch.ones(100) # class1 y data (tensor), shape=(100, 1)#拼接数据x = torch.cat((x0, x1), 0).type(torch.FloatTensor) # shape (200, 2) FloatTensor = 32-bit floatingy = torch.cat((y0, y1), ).type(torch.LongTensor) # shape (200,) LongTensor = 64-bit integer# tensor类型 :张量:高维特征,不可更新#variable类型:可变参数,更新w,b从而更新xy# torch can only train on Variable, so convert them to Variablex, y = Variable(x), Variable(y)设计构建神经网络:class net 继承torch.nn.model
    200\*2, 2\*10, 10\*2参数:n_fea=2,n_hidden=10,n_output=2
    输入层2
    初始化hidden层10
    self.hidden=torch.nn.linear(n_fea,n_hidden)输出层2
    self.out=torch.nn.linear(n_hidden,n_output)class Net(torch.nn.Module): def __init__(self, n_feature, n_hidden, n_output): super(Net, self).__init__() self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer self.out = torch.nn.Linear(n_hidden, n_output) # output layer#激活函数,在隐藏层后加relu非线性变换 def forward(self, x): x = F.relu(self.hidden(x)) # activation function for hidden layer x = self.out(x) return x #输出的预测值xnet = Net(n_feature=2, n_hidden=10, n_output=2) # define the networkprint(net)参数梯度下降sgd:
    optimizer = torch.optim.sgd(net.parameters(),lr=0.02)损失函数:交叉熵:负对数似然函数,并计算softmax
    loss_func = torch.nn.CrossEntropyLoss()参数初始化:
    optimizer.zero_grad()反向传播:
    loss.backward()计算梯度:
    optimizer.step()for t in range(100): out = net(x) # input x and predict based on x loss = loss_func(out, y) # must be (1. nn output, 2. target), the target label is NOT one-hotted optimizer.zero_grad() # clear gradients for next train loss.backward() # backpropagation, compute gradients optimizer.step() # apply gradients if t % 2 == 0: # plot and show learning process plt.cla() prediction = torch.max(F.softmax(out), 1)[1] pred_y = prediction.data.numpy().squeeze() target_y = y.data.numpy() plt.scatter(x.data.numpy()[:, 0], x.data.numpy()[:, 1], c=pred_y, s=100, lw=0, cmap='RdYlGn') accuracy = sum(pred_y == target_y)/200. plt.text(1.5, -4, 'Accuracy=%.2f' % accuracy, fontdict={'size': 20, 'color': 'red'}) plt.pause(0.1)plt.ioff()plt.show()交叉熵问题:
    二次代价:

    a 是 神经元的输出,其中 a = σ(z), z = wx + b
    C求导:w,b求偏导:
    受到激活函数影响

    交叉熵函数:


    不用对激活函数导数影响,不管是对w求偏导还是对b求偏导,都和e有关,而和激活函数的导数无关,比如说sigmoid函数,只有中间下降最快,两边的下降逐渐缓慢,最后斜率逼近于直线
    反向传播算法:errorBP
    可应用于大部分类型神经网络
    一般说BP算法时,多指多层前馈神经网络
    上面的数据为了演示,造的假数据形成200*2维的矩阵,一列label
    那么如何对接我们的真实数据呢?

    在我们前几节的数据中大都是这种形式,label feature_id:score
    定义一个数组
    output =[None]*3 维护3列tensor数据:
    max_fid = 123num_epochs = 10data_path = '/root/7_codes/pytorch_test/data/a8a'all_sample_size = 0sample_buffer = []with open(data_path, 'r') as fd: for line in fd: all_sample_size += 1 sample_buffer.append(line.strip())output = [None] *3#idids_buffer = torch.LongTensor(all_sample_size, max_fid)ids_buffer.fill_(1)output[0] = ids_buffer#weightweights_buffer = torch.zeros(all_sample_size, max_fid)output[1] = weights_buffer#labellabel_buffer = torch.LongTensor(all_sample_size)output[2] = label_bufferrandom.shuffle(sample_buffer)sample_id = 0for line in sample_buffer: it = line.strip().split(' ') label = int(it[0]) f_id = [] f_w = [] for f_s in it[1:]: f, s = f_s.strip().split(":") f_id.append(int(f)) f_w.append(float(s)) #解析数据后填入数组 f_id_len = len(f_id)#适当拓展数组大小 output[0][sample_id, 0:f_id_len] = torch.LongTensor(f_id) output[1][sample_id, 0:f_id_len] = torch.FloatTensor(f_w) output[2][sample_id] = label sample_id += 1fid, fweight, label = tuple(output)fid, fweight, label = map(Variable, [fid, fweight, label])接下来是设计网络结构:

    def emb_sum(embeds, weights): emb_sum = (embeds * weights.unsqueeze(2).expand_as(embeds)).sum(1) return emb_sum#输入结构为22696条数据*124维特征*32维特征向量:可以将其想象成立方体的三维条边#想要把id的特征向量和对应的score相乘并相加,需要把score也就是权重weight拓展成和id同样的维度,即22696 *124 *32 ,而sum(1)就是把124维相加成1维,最后形成22696×32维的平面矩阵,即每个样本后有个32维的向量存在,相当于把feature_id向量化class Net(torch.nn.Module): def __init__(self, n_embed, n_feature, n_hidden, n_output): super(Net, self).__init__() self.embedding = nn.Embedding(max_fid + 1, n_feature) #124特征*32隐层向量 self.hidden = torch.nn.Linear(n_feature, n_hidden) # hidden layer self.out = torch.nn.Linear(n_hidden, n_output) # output layer def forward(self, fea_ids, fea_weights): embeds = self.embedding(fea_ids) embeds = emb_sum(embeds, fea_weights)#对特征进一步处理,融合id与weight_score进行相容 embeds = nn.functional.tanh(embeds)#非线性变换 hidden = self.hidden(embeds) output = self.out(hidden) return outputnet = Net(n_embed = 32, n_feature= 32, n_hidden=10, n_output=2) # define the networkprint(net)以上,我们对接了真实的数据类型,那这个demo有没有什么问题呢?
    1.是否支持大规模数据,进行快速训练?mini-batch
    我们之前学BGD、SGD、MGD梯度下降的训练方法,在上面就运用了sgd的方法,不管是BGD还是SGD都是对所有样本一次性遍历一次,如果想提升,大致相当于MGD的方法:
    把所有样本分批处理,每批次有多少个样本(batch),循环所有样本循环多少轮(epoch)。
    每训练一个batch产生一条log,参数可保存,之前是每epoch输出一次log,这种方法训练快,性能虽然赶不上全局跑出来的准确率,但仍然一直逼近,小步快跑,每次加载到内存的数据较小,性能方面就高。
    num_epochs=10batch_size =1000data_path = './data/a8a'max_fid = 123def read_and_shuffle(filepath,shuffle=True): lines=[] with open(filepath,’r’) as fd: for linr in fd: lines.append(line.strip()) if shuffle: random.shuffle(lines) return linesclass DataLoader(object): def __init__(self,data_path,batch_size,shuffle = True): self.batch_size = batch_size self.shuffle = shuffle if os.path.isdir(data_path): self.files = [ join_path(data_path,i) for I in os.listdir(data_path)] else: self.files = [data_path] def parse(self,line_ite): for line in line_ite: it = line.strip().split(‘ ’) label = int(it[0]) f_id = [] f_w = [] for f_s in it[1:]: f,s = f_s.strip().split(‘:’) f_id.append(int(f)) f_w.append(float(s)) f_id_len = len(f_id) output = [] output.append(f_id) output.append(f_w) output.append(f_id_len) output.append(label) yield output def to_tensor(self,batch_data,sample_size): output =[None]*3 ids_buffer = torch.LongTensor(sample_size,max_fid) ids_buffer.fill_(1) output[0]=ids_buffer weights_buffer = torch.zeros(sample_size,max_fid) output[1]=weights_buffer label_buffer = torch.LongTensor(sample_size) output[2] = label_buffer for sample_id,sample in enumerate(batch_data): f_id,f_weight,f_id_len,label = sample output[0][sample_id,0:f_id_len] = torch.LongTensor(f_id) output[1][sample_id,0:f_id_len] = torch.FloatTensor(f_w) output[2][sample_id] = label return tuple(output) def batch(self): count = 0 batch_buffer =[] for f in self.files: for parse_res in self.parse(read_and_shuffle(f,self.shuffle)): count+=1 batch_buffer.append(parse_res) if count ==self.batch_size: t_size = len(batch_buffer) batch_tensor = self.to_tensor(batch_buffer,t_size) yield (batch_tensor,t_size) count = 0 batch_buffer = [] if batch_buffer: t_size = len(batch_buffer) batch_tensor = self.to_tensor(batch_buffer,t_size) yield (batch_tensor,t_size)for epoch in range(1,num_epochs+1): for (batch_id,data_tuple) in enumrate(dataloader.batch(),1): data = data_tuple[0] all_sample_size = data_tuple[1] fid,fweight,label =data fid,fweight,label=map(Variable,[fid,fweight,label] optimizer = torch.optim.SGD(net.parameters(), lr=0.02) loss_func = torch.nn.CrossEntropyLoss() # the target label is NOT an one-hotted out = net(fid, fweight) optimizer.zero_grad() # clear gradients for next train loss = loss_func(out, label) loss.backward() # backpropagation, compute gradients optimizer.step() # apply gradients if batch_id % 2 == 0:#每两批次打印一次 # plot and show learning process prediction = torch.max(F.softmax(out), 1)[1] pred_y = prediction.data.numpy().squeeze() target_y = label.data.numpy() accuracy = sum(pred_y == target_y)/float(all_sample_size) print "epoch: %s, batch_id: %s, acc: %s" % (epoch, batch_id, accuracy)2.怎么使用模型?
    现在模型可以训练自己数据了,效果也提升了,那如何把模型的参数保存下来?
    print "epoch: %s, batch_id: %s, acc: %s" % (epoch, batch_id, accuracy) checkpoint = { 'model': net.state_dict(),#网络静态参数 'epoch': epoch, 'batch': batch_id, } model_name = 'epoch_%s_batch_id_%s_acc_%s.chkpt' % (epoch, batch_id, accuracy) model_path = join_path('./model', model_name) torch.save(checkpoint, model_path)拆分上面大文件出各个参数:

    import osimport sysimport torchimport torch.nn as nnimport randomfrom torch.autograd import Variablefrom os.path import join as join_pathimport torch.nn.functional as Fmodel_path = sys.argv[1]checkpoint = torch.load(model_path)model_dict = checkpoint['model']for k, v in model_dict.items(): print k model_sub_file_name = k model_sub_file_path = join_path('./dump', model_sub_file_name) f = open(model_sub_file_path, 'w') value = model_dict[k].cpu().numpy() value.dump(f) f.close()模型怎么用?加载、预测
    import sysimport numpy as npimport mathembedding_weight = np.load('dump/embedding.weight')hidden_weight = np.load('./dump/hidden.weight')hidden_bias = np.load('./dump/hidden.bias')out_weight = np.load('./dump/out.weight')out_bias = np.load('./dump/out.bias')emb_dim = embedding_weight.shape[1]def calc_score(fids, fweights): embedding = np.zeros(emb_dim) for id, weight in zip(fids, fweights): embedding += weight * embedding_weight[id] embedding_tanh = np.tanh(embedding)#对应相乘,第二维相加成10维向量 hidden = np.sum(np.multiply(hidden_weight, embedding_tanh), axis=1) + hidden_bias out = np.sum(np.multiply(out_weight, hidden), axis=1) + out_bias return outdef softmax(x): exp_x = np.exp(x) softmax_x = exp_x / np.sum(exp_x) return softmax_xfor line in sys.stdin: ss = line.strip().split(' ') label = ss[0].strip() fids = [] fweights = [] for f_s in ss[1:]: f, s = f_s.strip().split(':') fids.append(int(f)) fweights.append(float(s)) pred_label = softmax(calc_score(fids, fweights))[1] print label, pred_label3.怎么评估?auc
    cat auc.raw | sort -t$'\t' -k2g | awk -F'\t' '($1==-1){++x;a+=y}($1==1){++y}END{print 1.0 - a/(x*y)}'acc=0.827auc=0.842569acc=0.745auc=0.494206轮数、acc都影响着auc,数字仅供参考
    总结以上,是以二分类为例,从头演示了一遍神经网络,大家可再找一些0-9手写图片分类任务体验一下,这里总结下,模型拥有参数,所以涉及参数初始化,有了参数后涉及正向传递,分为拟合和泛化两部分建立好模型后,学习过程就是确定参数的过程,使用的是梯度下降法,定义一个误差函数loss function 来衡量模型预测值与真实值(label)之间的差距,通过不断降低差距来使模型预测值逼近真实值,更新参数的反向传递是利用对应的误差值来求得梯度(batch、epoch、learning rate 、shuffle),评估标准也根据任务类型和具体要求可以使用更多的指标。
    回归任务常用均方差MSE作为误差函数。评估可用均方差表示。
    分类任务常用交叉熵(cross entropy)和softmax配合使用。评估可用准确率表示。
    回归任务:

    借用知乎yjango末尾的一句话:
    但最重要的并非训练集的准确率,而是模型从未见过的测试集的准确率(泛化能力),正如高考,真正的目的是为了解出从未见过的高考题,而不是已经做过的练习题,学习的困难之处正是要用有限的样本训练出可以符合无限同类样本规律的模型。是有限同无限的对抗,你是否想过,为什么看了那么多的道理、听了那么多人生讲座,依然过不哈这一生,一个原因就在于演讲者向你展示的例子都是训练集的样本,他所归纳的方法可以很轻松的完美拟合这些样本,但未必对你将面临的新问题同样奏效,人的一生都在学习,都在通过观察有限的例子找出问题和答案的规律,中医是,玄学是,科学同样是,但科学,是当中最为可靠的一种。
    0  留言 2019-06-05 13:46:30
  • 机器学习 18、聚类算法-Kmeans

    前文链接:https://write-bug.com/article/2530.html
    上节 我们暂时实现了一下单机版推荐系统,并且串了下知识,这节介绍下聚类,说起聚类就得先提下监督和非监督式学习:

    监督式学习:我们之前学习的分类、回归问题中的排序模型LR、Softmax,包括后面的dnn,dt等等都是需要数据事先有一个标签label作为新特征的分类
    非监督式学习:而这个有些类似之前的推荐算法,并没有特定的类别作为比对

    而今天学习的算法聚类算法也是非监督式学习。
    聚类是什么?之前对于文本分类来说,需要事先设定好类别标签对新文本进行概率分类、对label对比,而这里的聚类则可以既不需要设定类别,也不需要事先设定文本中心,其可自动将数据划分到不同的类中,使相似的数据划分到相同的类中,这种方法可应用到几乎所有分类中,用户聚类、商品聚类、生物特征、基因、图像聚类等等分类。
    比如说,随机系统中有10000篇文章,我只告诉系统分为多少个类,其他什么都不告诉,这个系统就可以自动操作完成。
    事实上,就是对特征的向量化及空间划分。
    向量化如果想对一个文章、物品等作聚类,那聚类之前,如何表示文章呢?
    向量
    比如之前的tfidf,用一个向量来表示物品之后作cos、内积、计算质心等等
    质心:各个向量的点构成的空间的中心,即每个点到这个点的最短平均距离
    A:(a1, a2, a3)B:(b1, b2, b3)C:(c1, c2, c3)质心=( (a1+b1+c1)/3, (a2+b2+c2/3, (a3+b3+c3)/3)距离矩阵、相似度矩阵
    此矩阵就像之前的推荐算法中的矩阵,各个pair相似度的矩阵
    评估如何评估聚类效果?
    内部方法
    同类尽可能相似,不同类尽可能不相似

    分子为对象到质心距离和,越小越好
    分母为质心距离,越大越好
    外部方法(准确P、召回R)
    之前利用pr曲线找到最好的分类面,但是需要有监督的情况下才能用,这里如果想用,在特定情况下,比如说文章有固定标签,用这种方法训练后再告诉label评估效果,但是工作中几乎遇到的都是无标签的,所以这种方法是很鸡肋的方法,工作中基本用内部方法解决。
    F=PR /(P+R)聚类类别层次聚类


    自底向上凝聚聚类,从不同的对象凝聚结果形成聚合节点,成树状
    自顶向下分裂聚类,从一个对象分裂结果形成分裂节点,成树状

    优点:

    可通过阈值灵活控制类个数
    层次用于概念聚类,分类后人工对其打标签,抽象概念。

    凝聚实现:

    将每个对象样本分为一类,得到N个类
    找到最接近的两个类合并为一个类:两层for循环遍历所有样本,计算cos相似度
    重新计算新类与旧类之间距离:类别之间距离和类别与对象之间距离的计算方式一般使用组合链的方式进行计算:即类中质心距离(平均距离),而与之相对的有单链(最近距离)和全链(最远距离)两种方式,一般工作中就直接使用组合链方式
    重复直到最后合并为一个类
    复杂度O(n^3)

    非层次聚类:Kmeans
    层次聚类是一种需要将向量两两比较聚类的方法,但这种方法的计算时间过长,影响效率,而Kmeans是随机选择对象作为类别中心,然后优化这些中心点的位置,使尽可能收敛到和真实中心一致的算法。
    步骤:

    任意选择K个点作为初始聚类中心 (初始化类个数)
    根据每个聚类的中心,计算每个对象与这些中心的距离(cos、欧式距离),并根据 最小距离重新对相应对象进行划分

    欧式距离:A:(a1, a2, a3) B:(b1, b2, b3)距离=sqrt((a1-b1)^2 + (a2-b2)^2 + (a3-b3)^2 )
    重新计算每个聚类的中心 (更新质心计算)
    当满足一定条件,如类别划分不再发生变化时,算法终止,否则 继续步骤2和3
    复杂度:O(Ktn^2)K个点,n个对象,t次循环,n次计算质心

    之前介绍时为什么说是向量化并分割空间,下面这个图每个点都是空间中的向量点,把两个中心点相连,中心分割的垂线就是分割面,再迭代更新质心位置,从而分割空间


    一定条件:

    轮数
    WCSS损失函数:最小化簇内对象到质心的距离平方和

    wcss变小,即空间变化不剧烈

    缺点:过于依赖初始点选择,可能导致的结果大不一样
    解决方法:只能多训练几轮,求质心平均值或者选择最小的wcss的模型。
    模型:最终计算结果的K个中心点向量
    K选择,业务场景:
    举例:半个性化推荐,即热榜推荐列表只有一部分是个性化对用户的推荐(如视频网站首页上半部),但是当用户上亿,但热榜或首页展示目录只有10个,候选集合只有100个的时候怎么办?如何给大量的用户也做一个模糊的推荐?如何在100个候选集合中选择10个作为推荐列表?
    对上亿用户作聚类,可以随意聚几千个类,再用这几千个类的向量和每个物品向量作cos,选出分数前10的物品向量作为此用户类的候选集合。
    用户向量化:利用历史行为作用户画像:token作向量
    层次 vs Kmeans

    从上面两张图,我们可以看到右面Kmeans算法的暴力分割和缺点,在生活中的数据大部分都是球面原始数据,也是Kmeans的强项,左面的那种长条数据只能用层次聚类分辨出来,而与此同时左面五颜六色的单点都是噪音数据,前面说过层次聚类非常耗时,并且我们事先并不知道数据的分布,所以部分解决办法就是增加K类个数。
    Kmeans粒度比较粗,层次粒度比较细。
    聚类这种发现规律的分类效果永远无法赶上分类回归那种有监督式的效果,即有老师无老师的区别。如果效果不好,可以适当增加K个数增加准确率。
    0  留言 2019-05-29 15:55:53
  • 大数据 17、单机推荐系统

    前文链接:https://write-bug.com/article/2491.html
    通过前面介绍的算法基础,我们可以利用CB、CF算法作召回和粗排,利用LR作精排打分,最后关联出特定用户正在收听的特定音乐的推荐集合,最终形成可视化页面。

    上图是推荐系统的base版本,可通过前几节的知识串接起来,后面会陆续介绍可替换的组件及更多业务情况。
    我们可以看到用户在页面浏览时,可以通过点击、收听等行为我们进行user_id,item_id的收集,与此同时,我们使用用户的行为信息对检索引擎进行检索,即召回阶段的计算结果,之后我们需要对返回的候选集合进行精排,通过lr对用户特征+物品元数据信息作回归打分从而把筛选的候选集合再次排序,实现个性化。
    项目流程:
    1.数据准备
    用户画像数据:user_profile.datauserid,性别,年龄,收入,地域
    物品(音乐)元数据:music_metaitemid,name,desc,时长,地域,标签
    用户行为数据:user_watch_pref.smluserid,itemid,该用户对该物品的收听时长,点击时间(小时)

    首先,将3份数据融合到一份数据中,python gen_base.py 代码解析数据,实现对数据的拼接;得到数据merge_base.data。

    输入:user_profile.data music_meta user_watch_pref.sml 三份数据
    输出:merge_base.data 拼接后的数据

    也可用hive处理:
    create external table user_profile(user_id string,sex string,age string,money string,place string) row format delimited fields terminated by ',';load data local inpath '/usr/local/src/code/recsys_test/data/user_profile.data' into table user_profile;create external table music_meta(item_id string,name string,desc string,hour string,place string,tag string) row format delimited fields terminated by '\001';load data local inpath '/usr/local/src/code/recsys_test/data/music_meta' into table music_meta;create external table user_watch_pref (user_id string,item_id string,listen_hour string,hour string) row format delimited fields terminated by '\001';load data local inpath '/usr/local/src/code/recsys_test/data/ user_watch_pref.sml ' into table user_watch_pref;3张表数据融合为一份数据user_watch_pref user_id,item_id,listen_hour,hour,user_profile sex,age,money,placemusic_meta name,desc,hour,placeinsert overwrite directory '/data' row format delimited fields terminated by ',' select distinct(a.user_id),a.item_id,a.listen_hour,a.hour,c.sex,c.age,c.money, c.place,b.name,b.desc,b.hour,b.placefrom user_watch_pref as a, user_profile as c,music_meta as bwhere a.user_id = c.user_id and a.item_id=b.item_id;
    2.召回、CB算法在学习推荐算法时,我们可以发现,不管是cb还是cf都可以通过对mr倒排式的协同过滤算法对数据进行处理,只是token item_id,score和user item_id,score 的区别。
    以token itemid score形式整理训练数据
    gen_cb_train.py 我们这里的代码对item的name,desc,tag进行分词并给予不同的权重便可以形成我们的数据。

    输入:merge_base.data 总数据
    输出:cb_train.data 3列数据

    要点:对item_id 进行去重,对不同元数据分词分数给予不同权重,如果音乐名字、描述、标签中出现相同的token,我们就把相应的分数乘各自的权重相加,如果不相同便加入token字典
    RATIO_FOR_NAME = 0.9RATIO_FOR_DESC = 0.1RATIO_FOR_TAGS = 0.05用协同过滤算法跑出item-item数据 mr_cf倒排式
    最后得到基于cb的ii矩阵,这里我也用hive弄了下,但是没想到怎么整理出ii矩阵:
    cb_train.datatoken,item_id,tfidfcreate external table cb_train (token string,item_id string,tfidf string) row format delimited fields terminated by ',';load data local inpath '/usr/local/src/code/recsys_test/data/cb_train.data into table cb_train;token,item_id,scoresum(sqrt(sum(s^2))):select token,item_id,sum(power(tfidf,2)) over(partition by item_id) as scorefrom cb_train order by item_id limit 100;s/sum(sqrt(sum(s^2))):insert overwrite directory '/data/cb_z' row format delimited fields terminated by ',' select t.token,t.item_id,cb_train.tfidf/sqrt(score) as f_scorefrom(select token,item_id,sum(power(tfidf,2)) over(partition by item_id) as scorefrom cb_train)t,cb_trainwhere t.item_id=cb_train.item_id and t.token = cb_train.token;limit 300;暂存:token,item_id,scorecreate external table cb_train_score (token string,item_id string,score string) row format delimited fields terminated by ',';load data inpath '/data/cb_z/000000_0'into table cb_train_score;对数据格式化,item-> item list形式,整理出KV形式
    gen_reclist.py并标记数据计算算法:

    输入:cb.result ii矩阵
    输出:cb_reclist.redis 粗排、标记、聚合

    类似如下数据:
    SET CB_5305109176 726100303:0.393048_953500302:0.393048_6193109237:0.348855灌库(redis)
    下载redis-2.8.3.tar.gz安装包,进行源码编译,执行make,然后会在src目录中,得到bin文件(redis-server 服务器,redis-cli 客户端)。
    启动redis server服务:
    ]# ./src/redis-server然后换一个终端执行:]# ./src/redis-cli,连接服务
    接下来灌数据(批量灌):
    需要安装unix2dos(格式转换)
    ]# cat cb_reclist.redis | /usr/local/src/redis-2.8.3/src/redis-cli --pipe验证:
    ]# ./src/redis-cli执行:
    127.0.0.1:6379> get CB_5305109176"726100303:0.393048_953500302:0.393048_6193109237:0.348855"3.召回、CF算法以userid itemid score形式整理训练数据 gen_cf_train.py
    得到userid对item的最终分数,即user收听时长表示对item的喜欢分数 score = float(t_finished) / float(t_all)
    用协同过滤算法跑出item-item数据 mr_cf,最后得到基于cf的ii矩阵
    对数据格式化,item-> item list形式,整理出KV形式
    unix2dos cf_reclist.redis灌库
    cat cf_reclist.redis | /usr/local/src/redis-2.8.3/src/redis-cli –pipe与上面cb算法一样流程.
    4.为lr模型作特征提取:
    user: gender、age
    item:name_token
    输入:merge_base.data
    输出:

    samples.data 特征提取数据user_feature.data 用户特征数据,为使用模型作数据准备item_feature.data 物品特征数据,为使用模型作数据准备name_id.dict 为包装数据作准备

    这里的label值用用户的收听时长来作判断:
    # label info label = float(watch_time) / float(total_time) final_label = '0' if label >= 0.82: final_label = '1' elif label <= 0.3: final_label = '0' else: continue用户特征分数初始化为1,物品特征分数运用jieba分词的idf作为初始分数
    形成如下数据:



    模型使用时,只有w,b 怎么使用,我需要有个库去存储user_fea、item_fea。实时拼接出特征,再通过model预测。
    用sk-learn进行模型训练,得到w,b。解析samples数据,还原矩阵:

    target_list:存储每个样本的label标签
    fea_row_list:存储样本行信息
    fea_col_list:存储样本列信息
    data_list:存储真实数据score

    转换数据并分割数据集:
    fea_datasets = csr_matrix((data, (row, col)), shape=(row_idx, max_col + 1))x_train, x_test, y_train, y_test = train_test_split(fea_datasets, target_list, test_size=0.2, random_state=0) # 28分为测试集及训练集训练
    x_train, x_test, y_train, y_test = load_data()model = LogisticRegression(penalty='l1')model.fit(x_train, y_train)for w_list in model.coef_: for w in w_list: print >> ff_w, "w: ", w for b in model.intercept_: print >> ff_b, "b: ", b print "precision: ", model.score(x_test, y_test)#预测print "MSE: ", np.mean((model.predict(x_test) - y_test) ** 2)#误差平方根auc评估
    for y in y_test: print >> ff_t, yfor y in model.predict_proba (x_test): print >> ff_p, y
    评估:auc:paste T.txt P.txt >auc.txt,awk无图形测试:但是需要回归的浮点分数不能用分类的label
    回归:model.predict_proba()
    分类:model.predict() :0&1

    5.推荐引擎及可视化通过前面的层层处理,我们得到了user&item特定的候选集合如何随意来了一个用户推荐个性化呢?

    解析请求:userid,itemid,pyweb模拟页面
    加载模型:加载排序模型(model.w,model.b),加载并解析数据
    检索候选集合:利用cb,cf去redis里面检索数据库,得到候选集合。截断粗排候选集合,得到item_list
    获取用户特征:userid
    获取物品特征:itemid,这两步用到上面说的特征库
    打分(逻辑回归),排序。对拼接出的特征进行检索wx计算,更新分数,并通过sigmoid函数压缩分数
    top-n过滤
    数据包装(itemid->name),返回

    之后,再把最终的候选集合列表,post到前端页面。
    此上,即为单机版本的推荐系统实现,但是在工作中思路大概是这样,只是拓宽了算法组件、机器、备份、分流等等东西,需要多个部门共同协作。
    0  留言 2019-05-27 18:11:17
  • 机器学习 16、分类算法 -Softmax

    前文链接:https://write-bug.com/article/2467.html
    梯度下降BGD
    在前文中,我们通过梯度下降法最优化更新参数w,那么上节那种迭代的方法为BGD,批量梯度下降。这种方法每次都把所有数据集都遍历一遍,从而每次都可以更新最优的参数。这种方式比较慢,实用性差但是效果好。

    SGD
    正常工作中,我们通常使用SGD,随机梯度下降,此种方法快,使用当前数据集的最优参数通过多次迭代不断逼近全局最优值。

    MBGD
    工业界中,常用方法minibatch小批量梯度下降,把所有样本分为不同的批次依次执行BGD(参数不变一直更新同一参数),效果介于前两种之间的折中方式。


    高参:不是模型参数,又影响训练结果的因素,根据不同的任务,人工调节
    Learning rate:参数以多大的幅度利用梯度更新,即走的步长epoch:遍历所有样本次数Batch size :每个batch中包含多少个样本的个数
    Batch:批次,每次epoch中,包含多个batch
    Step:更新一次批量
    shuffle优化:每次取的样本做随机打乱

    更新过程:
    计算误差值,将误差作为参数计算梯度,将所有参数减去对应梯度乘以学习速率完成一次更新
    多分类Softmax逻辑回归是Softmax的一般形式。在二分类中我们求取p(y=1|x)=sigmoid(wx)的概率作为正例,同时只求一组w,b的权重就可以了。
    但是在多分类中,我们的分类集合为{1,2,3,4。。。。。k}应用:数字识别、文章分类等
    那么这时,我们就需要对这个样本属于每个分类都求取一组权重:
    P(y=1|x)—w1=e^(w1x)P(y=2|x)—w2=e^(w2x)P(y=3|x)—w3=e^(w3x)P(y=1|x)=w1/(w1+w2+w3)所以求取到的每个概率看那个概率最大就属于哪个类。
    在给定输入x,对每一个类别j估算出概率值为p(y=j|x)
    输出一个k维向量(元素和为1)来表示这k个估计的概率值

    向量中所有概率相加=1。
    我们知道,逻辑回归的损失函数是

    那么由此我们可推广出softmax回归损失函数:


    k:当前类别,m:真实label
    只不过是softmax对k个值进行累加,logic对2个值进行累加
    同样的,对损失函数求导得:

    这时,你可能会好奇,到底怎么logic就成了softmax的特殊情况了,若k=2,Softmax回归退化为logistic回归:

    两个参数向量均减去向量θ1:

    此时,就得到了两个参数组成的向量,此值就是原来的概率值。
    我们在上面得到了梯度公式,就可以通过每个类乘学习率,各自更新自己的权重,得到一列权重向量。
    L1、L2正则我们可以看到上面提出的三种梯度公式都有一个λw,而这个λw可加可不加,加了之后相当于每次迭代更新时都变相给各自的权重加了个约束,对于大部分训练,效果都会变好,即正则化项。

    λ:衰退权重
    常用正则化项:

    L1范数:λ|w| 又称lassoL2范数:λ|w|^2 又称ridge(岭回归)当然在训练中,L1和L2是可以加起来跑的。L1+L2:(梯度+L1+L2)

    在日常使用中,我们需要对所有维度做一个综合考虑,达到效果最好。假设分辨男女的特征,类似戴眼镜这种没有区分能力的特征我们需要把他的w学为0,还有如果我的w声音学到了1000,w身高学到了100,w体重学到了10,单纯看每一个特征它的w都比较合理,但是从整体来看,w小的特征就会被强特征淹没掉,比如前面这几个只要出现了声音特征可能就一定确认为男性。这样的效果并不是我们想要的,泛化能力弱。
    这个正则项就是对w的惩罚项,如果加了正则项就相当于压缩了w:
    L1:|w|<1L2:sqrt(|w|^2)<1W是一组向量在公式中与各自的X相乘作内积,那上面压缩了w就相当于对每个w乘一个相同小数,Wold和Wnew比例关系相同,那么和之前的wx就是一回事了。


    由上图,我们把每次w更新的值想象为等高线(效果相同),在同倍数缩小时,和L1,L2构成的图形相交(每次更新的w都和我们的λ有着直接联系,看如何选择最优那组w)达到限制w扩散过大的目的。同时从图形中我们可以看出一些特点:

    L1:w1+w2=1,某时w与其角上会重合,过小的w会被优化为0,形成稀疏矩阵
    从系统实现角度:大大减少参数存储空间(只存储有数部分)、针对性强,泛化能力好,防止过拟合
    L2: w1^2+w2^2=1,与圆相交相切几乎都不为0,不会产生稀疏矩阵,可以对一票否决权的这种权重惩罚的也大,让权值尽可能小,防止过拟合

    欠拟合、过拟合在大部分算法中,都存在过拟合问题,即一个模型在训练集上表现得非常好,但是在实际测试中效果差,将噪声一起学到了模型中

    上图中,既可以表现过拟合的现象,又是一种防止过拟合的方法:即交叉验证
    上面的线为测试集错误率,下面为训练集错误率,在训练的同时进行测试(每训练几轮测试一下),一旦测试集错误率上升,立即停止训练,防止过拟合。
    那么还有什么防止方法呢?

    筛选特征(过多特征的筛选、降维)
    人工筛选:根据时间、重要程度等等对特征进行优先级排列或筛选
    机器筛选(如mrMR信息增益(-决策树)):比如一个特征对男女判断是否有用,可以对其求auc

    特定模型方法
    增加惩罚项(L1、L2)
    决策树高度

    增加训练数据(万能)
    神经网络Dropout(减边)

    欠拟合:一般都是训练轮数不够,那么一般增加训练轮数和决策树拓展分支
    1  留言 2019-05-16 13:39:17
  • 机器学习 15、分类算法 -LR

    前文链接:https://write-bug.com/article/2439.html
    逻辑回归前节中,我们分析了什么是分类以及机器学习的模型介绍、NB算法的应用、和二分类模型的评估。我们在这里再次总结一下机器学习的要素(括号内是对于LR而言):

    机器学习用数据训练出模型再应用的过程
    训练集训练所用的输入与输出
    测试集评估所用的输入与输出
    监督学习机器学习中,分为监督学习和非监督式学习,而监督式学习就是数据集有着明确的答案(即label),可供后续寻找输入输出之间的关系(误差)
    模型描述输入输出之间关系的方式(等式参数)
    训练学习输入输出之间关系的过程
    预测解决输入的预期输出的过程
    评估衡量模型好坏的过程
    泛化能力对一个前所未见的输入可以给出一个正确的预测

    在谈LR之前,我们需要介绍一些铺垫知识:
    一元线性回归何为回归为题?假如有一二维坐标系,横坐标x:西瓜重量,纵坐标y:西瓜价格,上面散落着一些点,即每个西瓜重量都有着对应的价格。如下图:

    那我如何根据这些数据,获取到一些规律,每次随意给我一个西瓜重量我都可以预测一个合理价格出来呢?我们需要像图中绿色的那条线一样,尽力去拟合所有数据,即 y=wx+b,给定x西瓜重量,返回y西瓜价格,未知参数:w,b。
    在上图中我们可以看到很多data点,同样的也就可以画出很多条线,那我们如何才能找到最可以拟合所有数据的最优线呢?我们可以给每个点和每条线之间都求取点到线的距离,即线没有拟合点的误差值,那么当我们找最优线的时候,我们把每次和这条线求的所有误差值加起来,看谁最小就好了,即最小化误差平方和:
    ∑((wx+b) - y)^2即loss function 找出最优参数,获取最优线。
    Error = y - y^那么如何求取最优参数?因为lossfunc为二次方程,坐标系中为下凸函数,所以求导=0就可以得出极小值:最优参数w,b

    模型输出:y^=wx+b
    模型参数:w,b

    多元线性回归在一元中,我们输入x为一个单因素值,即标量。在多元中,我们输入的x为多因素值,即向量。那么什么情况为多因素值呢?比如还是西瓜,那我的西瓜价格不只是重量影响着,还有颜色,拍的响声等等,而这些因素就是西瓜的特征,而西瓜在这里成为样本。
    但是这种多维度向量并不能在二维坐标系中显示,每个样本都有着多维度特征,可以想象,像二维坐标系中显示一样,在高纬度空间中散落着这些点,如果要做回归问题,就需要有一个超平面拟合这些数据点,如果做分类问题则需要一个超平面去尽力把两类样本点分开,而这里的西瓜问题是个回归问题。而这个超平面也就意味着我们的参数也变成了多维向量。即:
    X[x1,x2,x3...xn]W[w1,w2,w3,...wn]wx向量相乘是求内积。
    之前我们的等式y=wx+b,在机器学习中有了些新的名称:

    X:样本特征向量
    W:权重向量
    B:偏置

    比如:

    X样本为一个西瓜,那么西瓜的特征可能有:重量、颜色、响声、把的位置等等
    W权重即这些特征的重要程度,每个特征的权重都影响着最后的score
    B偏置的意思就是比如说各个地区的西瓜长得样子普遍不一样,那对于这类样本来说给他一个惩罚值平衡样本数据

    同样的,我们得到多元回归方程:
    𝑓(𝑥𝑖,𝑤,𝑏) = ∑𝑤𝑗𝑥𝑖𝑗 + 𝑏 = [𝑥𝑖1 ⋯ 𝑥𝑖𝑘][𝑤1 ⋮ 𝑤𝑘]+ 𝑏 = 𝑤𝑖𝑥 + 𝑏我们把b约去得到:
    𝑓(𝑥𝑖,𝑤,𝑏) = [𝑥𝑖1 ⋯ 𝑥𝑖𝑛][𝑤1 ⋮ 𝑤𝑛]+ 𝑏 = [𝑥𝑖1 ⋯ 𝑥𝑖𝑛 1][𝑤1 ⋮ 𝑤𝑛 𝑏]= 𝑤 · 𝑥𝑖所以只需要求:
    𝑓(𝑥𝑖,𝑤) = 𝑤𝑥和前面一样,为了求取最优参数,需要最小化误差平方和:
    min 𝑤 𝐿(𝑤) = ∑ (𝑤𝑥𝑖 – 𝑦𝑖)^2------》e只不过单因素标量变成了多因素向量了而已。
    同样,求导=0,但是这里w也是个向量, 根据微积分公式需要对每个w特征权重求偏导,得出:
    W=(XTX)^-1 XTYXT为X向量的转置,Y为输出后的向量
    当XT X 不可逆时,设定正数λ,W=(XTX + λI)^-1 XTY
    逻辑回归逻辑回归,logistic回归,是一种广义的线性回归分析模型,常用于数据挖掘,疾病自动诊断,经济预测等领域。常用于二分类问题,而多分类问题常用SoftMax函数来解决。
    上节说过,二分类问题也就是01问题,比如判断一个人是男是女,可以从多个特征来分辨:肤色、身高、体重、音色等等,每个特征都有自己的权重,像前面一样代表各个特征的影响度。
    设定1为男性,0为女性,则P(y=1|x)代表男性的概率,P(y=0|x)代表女性的概率。通常情况下,我们研究正例的概率,也就是y=1的概率,那么有:
    P(y=1|x)=f(x)这里的输入和前面的多元线性回归一样,但是输出是一个score,即
    f(x)=wx但是这里的score也就是y是一条直线,值域是(-∞,+∞),而概率的值域是[0-1],那如何让P(y=1|x)=wx呢?一起来推导下:
    exp(wx)的值域是[0,+无穷)p(y=0|x) = 1 - p(y=1|x)p(y=1|x) / (1 - p(y=1|x))的值域是[0,+无穷)p(y=1|x) / (1 - p(y=1|x))= exp(wx)p(y=1|x) = 1/(exp(-wx) +1)以上,我们通过压缩变换值域把他强制变到一个0-1的概率值
    在实际应用中,是客观存在这种非线性变换函数的,这里LR用Sigmoid函数:

    可以发现,和我们上面的想法推导几乎一致,但是这个函数不是我们推导出来的,而是客观存在的函数:

    那么这里我们求w这个最优参数值呢?
    在NB朴素贝叶斯中求取概率值,我们运用最大似然估计参数从而求得概率值。
    那么在这里我们同样对P运用最大似然:
    P(Y|X)=P^y * P^(1-y)这么表示并无实际含义,只是当y=0或1时的概率,那我们对它使用极大似然估计参数w:
    MLE w = arg max log(P(Y|X)) = arg max Π P(yi|xi) = arg max ∑ log P(yi|xi) = arg max ∑(yi LogP(y=1|X) +(1-yi) LogP(y=0|X))p(y=1|x) = 1/(exp(-wx) +1)p(y=0|x) = 1 - p(y=1|x)所以有:
    L(w)=-∑(yi log(𝜂(wx)) +(1-yi) (1-log(𝜂(wx))))为什么前面有个减号呢?最大似然函数是求取参数的极大值,是上凸函数,前面加个符号,图像就变成了下凸函数,这样我们就可以求取到参数极小值。
    看到上面的式子,如果你熟悉信息熵的话,是可以直接得到上面的式子的,即
    MLEmax = lossfunction min (- min CrossEntropy)信息熵公式:
    H(x)= - ∑ P(xi)log(p(xi))后面我们会有地方介绍信息熵,我们得到上面的L(w)负对数似然公式,由于它是高阶连续可导下凸函数,此时就可以将他作为损失函数来求解:
    利用梯度下降法更新参数w,求导得:
    ▽L(w)= -∑(yi- 𝜂(wx))xi机器学习的损失函数是人为设计的,用于评判模型好坏(对未知的预测能力)的一个标准,就像去评判任何一件事物一样,从不同角度看往往存在不同的评判标准,不同的标准往往各有优劣,并不冲突。唯一需要注意的就是最好选一个容易测量的标准,不然就难以评判了。
    其次,既然不同标准并不冲突,那使用最小二乘作为逻辑回归的损失函数当然是可以,那这里为什么不用最小二乘而用最大似然呢?请看一下最小二乘作为损失函数的函数曲线
    J(θ)=∑(yi- 𝜂(θx))
    以及最大似然作为损失函数的函数曲线
    L(w)=-∑(yi log(𝜂(wx)) +(1-yi) (1-log(𝜂(wx))))
    很显然,图2比图1展现的函数要简单多,很容易求到参数的最优解(凸函数),而图1很容易陷入局部最优解(非凸函数)。这就是前面说的选取的标准要容易测量,这就是逻辑回归损失函数为什么使用最大似然而不用最小二乘的原因了。
    此时,我们就可以和前面一样通过lossfunc求取最优参数w,通过不断降低差距来使预测值接近真实值(即反向传递的误差):
    由于L(w)是高阶连续可导的凸函数,根据凸优化理论,可利用梯度下降法、 牛顿法等求解,求导得到梯度:
    ▽L(w)= -∑(yi- 𝜂(wx))xi梯度:大小为导数的值,方向指向误差上升最快的方向
    当参数w维度大于1时,用相同的方法对各个维度求e偏导数(e(w)’),偏导数所组成的向量就是梯度。
    导数和梯度的区别是:导数是变化率,而梯度是一个和参数维度一样的向量。
    各个维度的数值等于对应的偏导数,那么让参数向梯度相反的方向更新,从而减小误差值。所以,使用梯度下降法更新最小化F(W),从而更新参数W:

    设置初始化w,计算F(w)=sigmoid(wx ) ->预估值我们的数据前面说过都是有label值的,也就是真实值,得到预测值和真实值之间的误差,我们希望通过学习数据让误差越来越小。
    计算梯度
    ▽F(w)=▽L(w)= -∑(yi- 𝜂(wx))xi此时我们已经得到了y真实值,w初始化值,x样本特征值,可以发现,梯度式子里面就是一个误差公式,那我们通过计算就可以直接代入乘以样本向量,得到梯度。下降方向
    dir = -▽F(w)负号:前面说过,反方向是下降最快方向
    尝试梯度更新
    𝑤𝑛𝑒𝑤 = 𝑤 +步长∗ 𝑑𝑖𝑟 得到下降后的 𝑤𝑛𝑒𝑤和F(w𝑛𝑒𝑤)如果 F(w𝑛𝑒𝑤)- F(w)较小,停止; 否则 ;跳到第2步
    1  留言 2019-05-08 22:26:17
  • 机器学习 14、分类算法-NB、AUC

    前文链接:https://write-bug.com/article/2428.html
    分类算法更新:NB、PR/AUC/ROC、LR、Softmax、DT、RF、GDBT、DNN、CNN、RNN
    什么是分类问题呢?
    最常见的机器学习任务:给定一个对象X,将其划分到预定义好的某一个类别Yi。
    比如说:性别识别,人群划分,新闻分类,query分类,商品分类,网页分类,垃圾邮件过滤,网页排序等等。
    对于性别识别来说,分辨男女,也就是二分类问题,即判断0和1问题。
    那我们如何评判一个人是男是女呢?这时就需要特征来解决:肤色、身高、体重、音色等等等等,我们人类打眼一看什么特征大概就知道这个人是男是女了,那机器是如何学会这个技能的呢?
    我们可以把这个机器当成是一个幼儿园小孩,如果幼儿园老师告诉小朋友,0:老虎很凶猛,1:小猫很温顺,那这时来了个狮子,小孩大脑里就会自动的拿狮子的特征去比对所见过的东西,觉得有些像老虎,那就会自然而然地离它远一些了。
    这个技能就是泛化能力,也就是说来了一个新东西,如果我能预测的很准的话,那就说明这次机器学习的模型是效果很好的,是具有泛化能力的。
    如果我想对文章做分类,一篇文章可以有很多类别,即多分类,那其中的特征是什么呢?关键词token,我提取出一篇文章的关键词,很容易就可以看出这篇文章属于哪类,每个词都有着对每个类别的倾向性,比如说‘股市’大概率都是财经文章。
    机器学习模型
    我们人类可以通过自己的学习经验一眼就可以把分类问题做出来,如果有大量数据的时候我们就需要机器去判断,也就是机器学习。
    机器学习目的:模型,可以帮我们对数据做预测。
    如果我想要机器学习对新数据预测的分类准确率高的话,我们就需要两点:

    好模型(老师/算法)
    好数据(教材/训练集)

    * 好模型 ≠ 复杂模型:

    复杂模型和性能开销成正比,即实现程度
    过拟合,对训练集学习太过深刻会把训练集中的错误数据知识也学习到了,缺少泛化能力,对测试预估效果影响较大

    评估模型学习效果:PR/AUC/ROC
    朴素贝叶斯(NB)算法
    我们在中文分词那节的语言模型中,推导过贝叶斯定理:
    p(s|c)=p(c|s)p(s)/p(c)推导:
    p(s|c) p(c)=p(s,c)—联合概率(同时发生的概率)->p(c|s)p(s)=p(c,s)-> p(s|c) p(c)= p(c|s)p(s)-> p(s|c)= p(c|s)p(s)/p(c)那假如我们设定:
    X:一篇文章
    Y:文章类别集合{}
    yi:第i个类别
    xi:文章某token
    p(yi|X):一篇文章属于哪个类别的概率
    则有:p(yi|X)=p(X|yi)p(yi)/p(X)
    p(yi):先验概率,即常识,已知规律
    我们看上面的设定来说,p(yi)代表着这个类别的概率,也就是说假设我训练集有100篇文章,我提前知道军事类别50,财经类别30,体育类别20,那么我们的先验概率也就固定下来了,p(y=军事)=50/100,以此类推,那么等到我测试集进行预测时,我的先验概率也按照这个比例来分布。
    p(X):常数,一篇文章出现的概率就像我们原先说的一个人说的每句话一样几率都是一样的。
    所以,公式简化为:
    p(yi|X)=p(X|yi)p(yi)p(X|yi)=p(x1|y1)\*p(x2|y1)\*p(x3|y1)....也就是把大X分成不同的关键词,但前提是独立同分布。
    所以朴素贝叶斯的前提:独立同分布
    那这里我需要对p(yi|X)=p(X|yi)p(yi)的参数进行估计:即最大似然估计
    p(yi),如何计算呢?统计,说白了就是我们刚才举例子的方法求得的。
    p(X|yi),两种计算方法:
    p(xi|yi)=count(xi,yi)/count(yi)count(xi,yi):即特征(关键词)xi和类别yi在训练数据中同时出现的次数
    count(yi):即某类别总数
    p(谷歌|军事)=军事类文章中包含‘谷歌’这个词的文章个数 / 军事类文章个数p(谷歌|军事)=军事类文章中包含‘谷歌’这个词的文章个数 / 军事类文章中所有词个数通过上面的分析与推导,我们得到了两个参数,如果来了一篇文章我们去预测属于那个类别,即p(yi|X),让这两个参数相乘就ok了。这个结果是一个概率,这就意味着如果是二分类问题,p(y=0|X)=70%,p(y=1|X)=30%,即有多少分类就要求多少个概率值,取概率最大的数,最致信。这个结果就是后验概率。
    优点:– 简单有效 – 结果是概率,对二值和多值同样适用
    缺点:– 独立性假设有时不合理
    NB实践:
    1.python DataConvert.py data/ nb_data
    数据准备:分好词的几千篇文章
    数据合并和转换:标签和token
    数据编码:token编码为数字,分测试集和训练集
    2.python NB.py 1 nb_data.train model
    训练数据训练,得到model(前面的两个参数)
    3.python NB.py 0 nb_data.test model out
    测试数据用model作预测—》求log
    二分类评估效果
    那我们怎么去评估我们的分类效果呢?对于二分类来说,我们最初使用混淆表(混淆矩阵)的方式进行评测:


    准确度Accuracy:(C11+C22)/(C11+C12+C21+C22)
    精确率Precision(y1):C11/(C11+C21)
    召回率Recall(y1):C11/(C11+C12)

    这种方式不好看,我们来替换成例子来看下:


    准确度Accuracy:(50+35)/(35+5+10+50)=85%
    精确率Precision(y1):50/(50+5)=90.9%
    召回率Recall(y1):50/(50+10)=83.3%

    上面的例子很明确,通常在工作中一般注重这两个指标,正确率(精确率)和召回率:

    正确率:预测样本中,正确的样本所占的比例,即看军事列
    召回率:预测样本中,正确的样本占同一类别总的样本比例,看军事行

    那么什么指标合适,在日常生活中,有的是侧重于召回,有的是侧重于正确率,越靠近底层一般越侧重于召回,越往上,越侧重于精确,即Nosq库那块侧重召回,排序模型那里侧重于精确
    上面我们提出了混淆矩阵,得到了准确率和召回率:
    这里引出了:PR、ROC、AUC
    有了这个正确率和召回率,我们可以获得一个PR曲线,即:同时评估正确率和召回率的方法就是通过PR曲线,p代表正确率,R代表召回率但是这个PR曲线很容构造成一个高正确率或高召回率的曲线,很难保证两全齐美,一般准确率高,召回率就低,召回率高,准确率低,可以构成一个二维码坐标,纵坐标是正确率,横坐标是召回率,用来确定阈值thd

    那么用生活中的两类场景来举例子:
    搜索场景,保证召回的前提下,再提高准确率
    疾病检测,保证准确性前提,提升召回
    PR曲线是个评测指标,用来协助你去选择阀值的,这个阈值怎么理解呢?比如我几篇文章被预测为军事的概率分别是0.7、0.8、0.6,如果阈值设置为thd=0.5,那么这三篇文章全被分辨为正例,都是军事。通过对阈值的调参,我得到的混淆矩阵里面的数值不同。
    那么我们看下ROC曲线的评测指标:

    纵轴:真阳率,召回率,TP/(TP+FN)
    横轴:假阳率FP/(FP+FN)
    那么ROC曲线有什么用,其实ROC曲线是为了得到AUC,即ROC曲线下的面积,,不同的阈值生成不同的混淆矩阵数值,打一个点就得到下面的曲线

    但是这样计算比较麻烦,我们可以利用其他方式去理解AUC,即负样本排在正样本前面的概率。
    假如A 0.1 B 0.9 我们假设负样本排正样本前面的概率认为正确,即A在B前面,认为是一次正确,B排在A前面,认为是一次错误。也就是按照概率进行倒序排序,上面是最小的概率,那么理想状态下,所有负例应该排在正例前面才对。
    我们可以通过一个AWK来计算:
    cat auc.raw | sort -t$'\t' -k2g |awk -F'\t' '($1==-1){++x;a+=y;}($1==1){++y;}END{print 1.0-a/(x\*y);}'x*y是正负样本pair对,a代表错误的个数,a/x*y 错误的概率,1-a/x*y 正确概率
    解释一下这个linux命令,按照第二个模型打的分数进行循环判断,小的排在前面,大的分数排在后面,当有的分数是比较小,但是不是该类的,这个a就加一个y,y从0开始加,直到结束,能够找到有多少a,进而计算评估的正确率
    例如:来了个文章,我们假如是军事类为+1,财经为-1,当然这个文章是军事类文章,即+1,然后我们设置一个阀值为0,即分类预测的分数 >0 认为是+1,<=0 认为是-1,x是负样本的个数,y是所有正样本个数,a是错误的样本个数

    那么这个最差就是0.5,压根不知道好坏,评测是到底是正确的评测还是错误的评测。最完美就是1或者0了
    0  留言 2019-04-28 12:40:13
  • 大数据 13、推荐算法-CF

    前文链接:https://write-bug.com/article/2376.html
    推荐算法在大数据Hadoop平台2-2、MapReduce和上节的实践中都提到了检索或推荐系统的数据来源的底层来源,那么这里即是召回阶段的算法。算法目的是侧重召回候选集合,粗排。
    我们在看爱奇艺网站时,上面会有一些推荐的热榜栏目和个性化推荐栏目等,那么这里的基础算法是什么样的呢?
    推荐算法简单分为两种:基于内容和协同过滤(行为)推荐
    1.1 基于内容Content base1.1.1 引入Item属性的Content Based推荐
    在前面介绍过的实践中,我们利用token实现item的倒排,其实就类似找到了一些item和item之间的相关性信息。我们做的正排表的分词就是此图内容分析索引,而相关性计算就是倒排索引,把它保存在nosql后就可以直接查询了。

    通常在网站主页中,不会做大面积的个性化推荐,其透漏着隐私问题会给用户带来困扰。
    1.1.2 引入User属性的Content Based推荐

    这里我们需要把item的token改成可以描述我用户行为偏好的token。
    个性化:

    Step1:user搜索、点击、收藏过这些item时(也就是用户行为数据),可以用这些item的token 给 (用户分析) user做用户画像(兴趣建模)
    Step2:—->倒排索引—-》token—-》索引—->item
    Step3:相关计算cos、jaccard ∩/∪等
    Step4:排序

    1.2 基于协同Collaboration Filtering核心:User对Item的打分形成UI稀疏矩阵(即只存储有行为的数据)。
    存储可以类比原来的倒排索引:userid(key)—->(value)item item item
    原来的倒排:一个token被几个item同时包含。
    这里的倒排:一个用户对几个item有过行为,即被一个用户作用过的几个item有着相关性。

    那稀疏矩阵是什么样子呢?

    数据例:
    1,100001,5 1,100002,3 1,100003,4 1,100004,3 1,100005,3 1,100007,4 1,100008,1 1,100009,5 1,1000011,2这个稀疏矩阵就是UI矩阵,只存储用户有行为的数据,由此数据量可以大大减小:即倒排索引。
    随着时间的推移,日志服务器会积累越来越多的行为数据形成UI矩阵。通常中型以上的互联网公司,用户量最少一亿以上(一个用户多个账号),音乐量级几十万以上,所以这个UI矩阵很大很大,需要用稀疏矩阵存储。

    由这个UI矩阵我们知道,userA对ItemBCD有过行为,那么由前面倒排索引的知识,我们可以得到itemA被哪些用户有过行为,得到了userlist,那我把ItemB为key就得到了UserA的好友列表。例:微博
    根据user A的好友列表userBCD,可以得到每个user背后的item列表,把这些item聚合推荐给userA。例:个性化推荐
    根据itemA背后的user列表做相似度关联,把每个用户背后的item聚合,在和itemA相关联。例:非个性化推荐


    可由1我们得到user和user的矩阵即UU=UI*IU(UI的转置矩阵);可由3我们得到item和item的矩阵即II=IU*UI。由此我们引出了两种协同:User-Base CF和Item-Base CF。
    1.2.1 user-Base —- CF:—————外交假设:
    – 用户喜欢那些跟他有相似爱好的用户喜欢的东西
    – 具有相似兴趣的用户在未来也具有相似兴趣
    方法:
    – 给定用户u,找到一个用户的集合N(u),他们和u具有相似的兴趣
    – 将N(u)喜欢的物品推荐给用户.

    UI矩阵计算——————》 UU矩阵—————-用户之间相似度
    流程:转置,归一,cos
    目的:如果把一个user没看过的电影推荐给他,打一个分预测用户,对这个电影有多大的兴趣

    r(userid,itemid)=两个用户的相似度×另一个用户的打分 / 所有用户(随机选择一部分)相似度加和这个结果,只需要基于同一套算法(UU / II)的排序,不需要具体分数。
    1.2.2 item -Base —- CF:———自身历史行为假设:
    – 用户喜欢跟他过去喜欢的物品相似的物品
    – 历史上相似的物品在未来也相似
    方法:
    – 给定用户u,找到他过去喜欢的物品的集合R(u)
    – 把和R(u)相似的物品推荐给u
    UI矩阵计算——————》 II矩阵—————电影之间相似度
    流程:转置,归一,cos
    目的:如果把一个user没看过的电影推荐给他,打一个分预测用户,对这个电影有多大的兴趣

    r(userid,itemid)=两个item的相似度×该用户的打分 / 所有item(随机选择一部分)相似度加和什么时候用哪个算法?

    性能角度:谁维度小用谁
    领域:UB时效性(看是否火,用户之间相互影响),IB倾向于个性化(历史相似物品)
    实时性:IB更高:新行为导致推荐结果实时变化
    冷启动:UB需要时间离线计算,IB围绕产生新行为的物品无限推荐
    推荐理由:UB难解释

    冷启动三类:—————-新东西——粗粒度吸引

    用户:推荐热门排行榜、注册年龄性别等做画像粗粒度推荐、社交网站导入好友信息推荐、新登陆反馈标签推荐
    物品:新物品通过基于内容推荐——-CB
    系统:不太好解释的,用户画像(职业)和物品(不懂这东西)———引入专家知识(知识图谱,人工校验等)建立相关物品矩阵

    I I/UU 相似度计算公式:———————cos

    类比cos=向量内积 / 向量模 ------把两个劈开相乘
    ——————> 归一化 ui / ui^2———————->两个Rui 评分相乘
    比如上面的两个电影 M 和 T 的相似度是0.57
    U(i,j)=A B D
    R(ua,M)=5 , R(ua,T)=1R(ub,M)=1 , R(ub,T)=5R(ud,M)=4 , R(ud,T)=3归一化:
    25+1+16=42---根号42M:5/1/4/1+25+9=35------根号35T:1/5/3/M*T=实践:协同过滤————————给定一个item,推荐相关的item集合(II矩阵)
    倒排式、分块式
    倒排式流程:
    遍历排列组合同一个人观看的所有item,得到很多相似度的pair;如果两个物品被越多的人贡献过,相似度越准确,去除偶然性误点。也就是排列出所有组合pair对,-----有几个相同对,相当于被几个人贡献过。相同的pairkey会分在同一个桶里,计算相似度分数就会越准确---套公式(reduce中)

    Step1:归一UI矩阵:

    Map:
    矩阵输入数据:U,I,S输出:Key(i),Value(u,s)对
    Reduce:
    输入:Key(i),Value(u,s)对输出:Key(u),Value(i,s_new)对

    Step2:衍生 I I Pair对:

    Map:
    输入:Key(u),Value(i,s_new)对输出:Key(u),Value(i,s_new)对
    Reduce:
    输入:Key(u),Value list((i,s_new))对输出:Key_1(i) , Value_1(j,s_new_is_new_j)Key_2(i) , Value_2(i,s_new_is_new_j)

    Step3:result :

    Map:
    输入: Key(i) , Value(j,s_new_i*s_new_j)输出:Key<i,j> , Value(s_new_i*s_new_j)
    Reduce:
    输入:Key<i,j> , Value list((s_new_i*s_new_j))输出:Key(i) , Value(j,score)


    前面说了一些最基础的推荐算法,但是其中存在了一些问题,我们会在后面更新基础的聚类算法中指出。并且在其中我们使用倒排式实现了协同过滤,突出了MR批量计算和对大规模UI矩阵的批量处理,但在其中代码的缺点也很明显,我们输入的UI矩阵产生pair对时,会产生比UI大得多的数据,并会有一些重复数据,很容易爆掉内存,所以我们需要优化在UI矩阵中取物品需要对每一个用户的物品候选集合设置阈值,也就是pair对多少的阈值,如果说一个用户点击了几乎所有item,pair数据指数级增长,内存就会立刻爆掉。后面:聚类、MF(ALS、LFM、SVD、SVD++)、ANN
    2  留言 2019-04-25 11:23:42
  • 大数据 12、中文分词

    前文链接:https://write-bug.com/article/2367.html
    文本相似度一般在讨论相似度时,我们讨论相似度与距离之间的关系:相似度越低,距离越远
    我们这里先分为两类:
    语义相似:

    个人简介
    人物介绍

    解决方案:协同过滤(用户行为)、回归算法
    eg:用户搜索歌神时,大部分人点击张学友,由此两个词间便存在了某种联系
    字面相似:

    我吃饱饭了
    我吃不饱饭

    解决方案:LCS最长公共子序列、中文分词(词:token)
    eg:通过token列出词粒度集合,通过算法打分或者取交集求取相似度
    解决字面相似:
    1.cosine余弦相似度
    以前在中学学习余弦时都是基于二维坐标系:
    cos(θ)=a·b/|a|* |b|我们词粒度的维度是由token数量决定的,由此我们拓展到n维得:

    • 句子A:(1,1,2,1,1,1,0,0,0)
    • 句子B:(1,1,1,0,1,1,1,1,1)

    a,b即通过分词后得到的词粒度集合向量化
    注意:由分词得到的去重集合(BOW bag of word)一旦列举出后顺序不能变
    计算流程:

    通过TFIDF找出两篇文章的关键词
    每篇文章各取出若干个关键词,合并成一个集合,计算每篇文章对于这个集合中的 词的词频
    生成两篇文章各自的词频向量
    计算两个向量的余弦相似度,值越大就表示越相似

    TFIDF:找关键词
    1)TF(Term Frequency):词频
    即假设如果一个词很重要,那么它在这篇文章中会多次出现。但是,出现最多的不一定是最重要的,比如停用词:“的”“是”“在”。。。(可提前过滤)那么再次假设关键词:在当前文章出现较多,但在其他文章出现少。
    公式:
    TF1=某词在文章中出现次数 / 文章词总数(基数大) 越大越重要TF2=某词在文章中出现次数 / 该文章次数最多的词的出现的次数(基数小)TF越大,越重要
    2)IDF:反文档频率,即某一个词在某些文章中出现个数多少的衡量
    公式:
    log(语库文档总数 / 包含该词的文档数+1) > 0单调函数,x越大,y越平滑。出现少,分母越小,IDF越大,词越重要。
    score= TF*IDF像停用词这样的词,TF大,IDF小
    拓展:自动摘要
    1)确定关键词集合
    两种方法:
    1.score—Top10
    2.阈值截断>0.8
    2)那些句子包含关键词,取出这些句子
    3)对关键词排序,对句子做等级划分(含该关键词的分数线性加和)
    4)把等级高的句子取出来,就是摘要
    优化:关键词算法、中心思想、第一末尾自然段等等。
    tfidf:

    优点:简单快速,结果比较符合实际状况
    缺点:缺少通顺度,而且关键词不是唯一的权重,比如开头结尾,中心思想。

    单纯以“词频”做衡量标准,不够全面,有时重要的词可能出现的次 数并不多
    实践:
    1.分词
    2.数据预处理,把所有文章内容全部收集到一个文件中,每行为一篇文章
    convert.py input_tfidf_dir/ >convert.data3.MR批量计算IDF

    map.py: word list去重,每一个单词在一篇文章中出现一次计1
    reduce.py: wordcount + IDF计算

    得到token,score集合
    排序:cat part-00000 |sort -k2 -nr |head
    2.LCS 最长公共子序列:顺序性,不连续
    从字面的角度,衡量字面相似度的方法之一
    应用:

    推荐场景 , item推荐,推荐列表—-保持多样性,满足新鲜感需求,放在推荐引擎中可方便调控相似度阈值过滤(即候选集合后)
    辨别抄袭
    生物学家常利用该算法进行基因序列比对,以推测序列的结构、功能和演化过程

    – 字符串12455与245576的最长公共子序列为2455
    – 字符串acdfg与adfc的最长公共子序列为adf
    计算方法:
    暴力穷举:abc排列组合,取LCS,穷举搜索法时间复杂度O(2^m \∗ 2^n);
    动态规划法:
    抽象公式:

    当xy两字符串最后一位相等时+1,并循环求lcs,不相等时求左右各减一位的最大值,如果有一方为空循环停止,值为0
    代码二维数组:

    i和j为行和列,从上到下从左到右看,最开始都为空,行列都为0,接着从左到右有相同项左上角+1,不相同的左和上取最大值,最后得出最大序列数
    score=4len(x)=7len(y)=6sim(x,y)=4*2/6+7=0.615*2为了归一化实践:
    通过数组维护tokenlist,左右两个字符串按照上面形式计算。
    lcs适合粗粒度过滤,适合场景没有cos广泛因为机器学习中涉及到cos的地方太多了
    3.中文分词中文并不像英文,每个单词自动空格分隔,并且没有一个衡量标准好坏问题,词粒度分割不同,意思也就不同。
    我们前面一直提到搜索和推荐场景,在这里,推荐适合粗粒度的分词,因为需要关心词的语义去推荐,而搜索适合细粒度的分词场景,侧重于召回更多的item到候选集合。
    表示方法:
    那我们如果对于一句话做分词,人类可以一眼就看出来,那么对于计算机来说怎么表示,那我们假设,切开位置表示为1,其他位置为0;那么这句话表示为:有/意见/分歧——》11010本田雅阁/汽车——》100010那么这种表示方法对计算机确实很友好,但是对我们来说很头疼,于是通常情况下,我们用分词节点序列表示:有/意见/分歧——》{0,1,3,5}
    那么接下来的问题,这串索引怎么得到呢?这里有一种原始方法:最大长度查找(字典匹配)这个又分两种:前向查找,后向查找。加载词典:
    在查找过程中我们如果一个词一个词去匹配未免过于耗费资源,所以这里用一个加速查找字典的方法:trie树—数据结构 查找过程如下:• 从根结点开始一次搜索;• 取得要查找关键词的第一个字母,并根据该字母选择对应的子树并转到该子树继续进行检索;• 在相应的子树上,取得要查找关键词的第二个字母,并进一步选择对应的子树进行检索。• 迭代过程……• 在某个结点处,关键词的所有字母已被取出,则读取附在该结点上的信息,即完成查找。其他操作类似处理.时间复杂度 O(n)—-空间换时间数据结构,并且在python中的字典数据结构这种速度是近乎于O(1)
    匹配词典:
    前面这是加载词典,而匹配词典是下面几种方法:1、最大长度查找前向查找:先从头匹配第一个字,之后看第二个字是否在第一个字后面,以此类推,若不在则砍一刀,之后继续查找当前第一个字。后向查找:即语料库词为倒序,即反向建库通常情况下后向查找会更符合语义。2、我们把每个字都分开,之后在语料库中匹配出每种排列组合的可能,用前面说的索引序列表示,与此同时,我们把每种可能都用一条线首尾相连,而这么多条线就构成了一张图:即有向无环图,也叫DAG图。
    由此一句话的编码表示:DAG: {0: [0, 1, 3], 1: [1], 2: [2, 3, 5], 3: [3], 4: [4, 5], 5: [5], 6: [6, 7], 7: [7]}
    那么我们这些边该如何去切分和选择呢?
    由此引出:语言模型概念
    我们由语料库已经有了很多列举后的切分方案,那么我们由这些切分方案可以得到每条方案的条件概率,比如:C=本田雅阁汽车S1=本田/雅阁/汽车S2=本田雅阁/汽车则:P(S1|C)表示S1这条切分方案的条件概率
    目标:我们希望得到的就是最大概率的那条切分方案。
    那我们如何求取这条概率呢?:贝叶斯公式
    p(s|c)=p(c|s)p(s)/p(c)
    推导:p(s|c) p(c)=p(s,c)—联合概率(同时发生的概率)->p(c|s)p(s)=p(c,s)-> p(s|c) p(c)= p(c|s)p(s)-> p(s|c)= p(c|s)p(s)/p(c)
    P(c):即这句话的概率,但是人类说每句话的概率都是相同的,即常量。所以有:p(s|c)= p(c|s)p(s)P(c|s):即p(本田雅阁汽车|本田/雅阁/汽车)=100%,即不管哪种切分方案都可以还原出原始的句子。所以有:p(s|c)= p(s)目标:P(S)独立性假设,每个词之间无任何联系,都是独立的那么有:p(本田/雅阁/汽车)=p(本田)p(雅阁)p(汽车)也就是每个词token的概率:即TF——word count由于P(S1)>p(S2),所以选P(S1)
    那么由此我们选择概率最大的那条路线,就是这条句子的切分最佳方案。
    但是,我们概率相乘是很容易形成很小的小数的,造成向下溢出,所以有:
    求log后因为概率是小于1的数所以为负数,可比性高,并且加法运行速度比乘法更快。
    我们前面是假设每个token前后是无关联关系的,但在实际生活中词与词都是有关联的,所以我们刚才的概率模型就是一元模型(只考虑到了词频概率)。
    多元模型:一元模型(Unigram) :一个词的出现不依赖于它前面出现的词—3个参数P(S)=P(w1) P(w2)P(w3)…P(wn)二元模型(Bigram):一个词的出现仅依赖于它前面出现的一个词—3+6个参数P(S)=P(w1) P(w2|w1)P(w3|w2)…P(wn|wn-1)三元模型 (Trigram):简化成一个词的出现仅依赖于它前面出现的两个词。3+6+。。
    每次参数排列组合爆炸级增长。
    J i e b a 分词简介官方链接:https://github.com/fxsjy/jiebajieba用于中文分词,支持的文本编码格式为utf-8,支持的功能包括:中文分词、关键字提取、词性标注整体功能如下图:
    框架结构:
    第一阶段:加载词库,用trie树分割,生成句子中汉字所有可能成词情况所构成的DAG图—-FULL
    词表格式: token,TF,词性 ——-》trie树——>DAG图——》找概率Route概率:获得词频最大切分P= TF / total(TF总和)—》log(p)—-》负数总词数:Linux:awk汉字计数:cat dict.txt | awk ‘BEGIN{a=0}{a+=$2}END{print a}’—— 60101967(total)
    第二阶段:动态规划或者贝叶斯计算最大路径概率,找出最大切分组合,Token英文中文识别我们前面得到了每条边和每个汉字的概率(即所有可能词的概率),接着用倒序索引找最大路径(类似开始说的第二种表示方法)route: {0: (-33.271126717488308, 1),1: (-32.231489259807965, 1),2: (-23.899234625632083, 5),3: (-31.52324681384394, 3),4: (-22.214895405024865, 5),5: (-19.00846787368323, 5),6: (-8.7800749471799175, 7),7: (-8.800692190498415, 7), 8: (0.0, ‘’)}从后向前导,每种切分方案是单个字概率大还是词概率大,确定后的方案概率不变,继续向前导,求最大方案,并累加概率值,以此类推。所以最后方案:0-1,2-5,6-7
    第三阶段:所有单个汉字的传给——>HMM模型粘贴词库中没有的被切碎的词(未登录词)—->再传回来改回为词更新句子,Viterbi动态规划算法
    HMM:隐马尔科夫模型定位:针对单字的粘合
    马尔可夫模型每个状态只依赖之前有限个状态之前的二元模型可等价于一阶马尔可夫模型——即依赖前一个状态 p(w1,w2,w3,……,wn) = p(w1)p(w2|w1)p(w3|w1,w2)……p(wn|w1,w2,……,wn-1) =p(w1)p(w2|w1)p(w3|w2)……p(wn|wn-1) 例如: p(w1=今天,w2=我,w3=写,w4=了,w5=一个,w6=程序) =p(w1=今天)p(w2=我|w1=今天)p(w3=写|w2=我)……p(w6=程序|w5=一个)马尔可夫模型3类参数: 状态(对于分词来说,每一个token即一个状态) 初始概率(某开头词汇出现的文章次数 / 总文章数),例p(w1=今天) 转移概率(条件概率) 统计:(P(B|A)=2/3 A出现时B出现概率)
    马尔可夫模型缺点:只是对一串序列做建模做不了情况:两个序列:实时翻译、文字和词性、语音识别
    由此引出了HMM—-隐马尔科夫模型——生成式模型即对观察序列(O)和隐藏序列(S)进行分析处理
    状态序列(隐藏序列)由:《位置信息(BMES)+词性》B:begin M:middle E:end S:single——做分词我/今天/真高兴==》SBEBME词性:如果一个词是N词,那么他的字也是N词—做词性标注
    HMM有5类参数:状态序列(隐藏序列)M个*4=256初始概率 M个转移概率 M*M发射概率 M*N观察序列 N个=常用汉字
    jieba表:位置+词性+概率prob_start:初始概率—————-256个prob_trans:转移概率,从一个状态到另一个状态的概率值,词性一样prob_emit:发射概率,从状态到字的概率,每个位置和词性后面匹配的字的概率获取方法:文章统计(词性人工标注)在中文分词中,我们需要用大量的已经分好词的数据作为先验知识,在其中统计BMES找到一些规律prob,以便于我们后期利用工具做中文分词。
    假设有3个观察、3个状态:P(S1,S2,S3,O1,O2,O3)联合概率(同时发生)=P(S1)*P(O1|S1)*P(S2|S1)…. 初始、发射、转移我们的目标:给定HMM模型,我们输入观测序列,希望得到最优的状态序列即最优的分词策略P(S|O)=P(S,O) / P(O) P(O)=100%P(S|O)=P(S,O)所有遍历一遍时间复杂度:M^T
    最优路径:动态规划:Viterbi只需要关心当前状态与前一状态的最优路径(最大概率),两两计算,再次向后迭代score1: t位置的状态概率,score2:从t到t+1的转移概率概率,O:从状态到字的概率计算:score1初始概率*score2转移概率*O发射概率=score3新分数——-类似孩子长个复杂度:M*M*(N-1)
    实践:1.MR批量分词Client本地:代码、jieba包Datanode:数据文件分发—file模式—-代码内解压
    2.pyweb+分词Get、Post引用jieba方法
    3.中文分词完成倒排索引正排索引item-》token——》分词倒排索引token-》item——->召回
    (1)map正排:一对一:name->token—————————分词+jieba权重scoretoken1^Ascore token2^Bscore(2)map倒排:token-》name
    (3)reducetoken1-》name name name———>wordcount思路拼接字符串
    1  留言 2019-04-12 12:23:32
  • 大数据 Spark平台5-3、spark-sql

    前文链接:https://write-bug.com/article/2361.html
    Spark-Sql官网:http://spark.apache.org/docs/latest/sql-getting-started.html
    这里对Spark家族进一步介绍,偏入门实践,优化概念会少一些。
    我们在学习Hive时,本质上是把sql语句转换成MR job,节省工作时间,提升效率,其在存储数据时,分为这几个层次:table / partition / bucket / hdfs
    spark sql同样也处理结构化数据,把数据源传来的数据用表格的形式解析并维护起来,与此同时也可和Hive结合使用(数据存储在Hive中)
    在spark streaming中我们通常开发一个模板——Dstream, SparkStreamingContext
    spark SQL同样也有类似的概念——DataFrame, 当成一个table(关系型表格)

    外部数据源(SQLContext):HDFS、网络接口、Mysql等
    Hive数据源(HiveContext):Hive

    两者关系:HiveContext继承自SQLContext,HiveContext只能支持HQL,SQLContext支持语法更多
    DataFrame(由很多RDD组成)让数据处理更为简单,是一个分布式的Table

    与RDD区别:传统RDD以行为单位读数据,DataFrame基于列的内部优化
    与RDD相同点:都具备懒惰机制(基于RDD的抽象)


    Spark SQL处理核心:Catalyst工作流程(本质:把sql、dataframe相结合,以树tree的形式来存储、优化)

    把sql语句和Dataframe输入形成未解析的逻辑计划,加上元数据Catalog的支持,形成逻辑计划,再经过优化,形成物理计划,最后再通过代价模型转化成可执行代码。
    优化点

    基于规则

    一种经验式、启发式优化思路(如sql语句优化)join算子——两张表做join
    外排
    大循环外排:A、B,两张表都很大,O(M*N)——不用游标式外排:归并排序(指针滑动比大小)
    内排:小表放内存,大表做遍历(hive中的mapside join)

    基于代价

    评估每种策略选择的代价,根据代价估算,确定代价最小的方案代价估算模型——调整join的顺序,减少中间shuffle数据的规模

    catalyst工作流程

    parser:针对sql解析
    词法分析:讲输入的sql语句串解析为一个一个的token
    语法分析:再词法分析基础上,将单词序列组合成各类语法短语,组成各个LogicPlan

    SELECT sum(v) FROM( SELECT score.id, 100+80+score.math_score+score.english_score AS vFROM people JOIN scoreWHERE people.id=score.id AND people.age>10 ) a

    analyzer:借助元数据(catalog)解析根据元数据表解析为包含必要列的表,并且相应字段解析为相应的数据类型,相应的计算逻辑解析为对应的函数


    optimizer:基于规则的优化策略经典规则:谓词下推(Predicate Pushdown)、常量累加(Constant Folding)和列值裁剪(Column Pruning)谓词下推:把过滤条件放在join之前执行

    常量累加(180)、列值裁剪(提前过滤掉不用的列值字段):


    物理计划:基于代价的优化策略用物理操作算子产生一个或者多个物理计划。然后用cost模型选择一个物理计划。目前基于cost-based的优化仅仅用 于选择join算法:对已知的很小的relations,sparksql会选择使用spark的提供的点对点的广播功能实现Broadcast join
    执行计划查看方式:

    Spark网页sql
    sql语句后面追加 . queryExecution方法查看

    官方:catalyst优化引擎,执行时间减少75%
    内存管理:Tungsten 内存管理器—— off-heap(堆外内存)

    本质:突破JVM内存管理限制,分配堆外内存(GC、与磁盘做交换dump)
    JVM:GC带来时间开销,可能出现“假死”情况

    实践
    1、基本demo:
    读数据:
    1)从hdfs的原始text中读数据:sqlTest
    //建立学生表Schemeval StudentSchema: StructType = StructType(mutable.ArraySeq( StructField("Sno", StringType, nullable = false), StructField("Sname", StringType, nullable = false), StructField("Ssex", StringType, nullable = false), StructField("Sbirthday", StringType, nullable = true), StructField("SClass", StringType, nullable = true) ))val sparkConf = new SparkConf().setMaster("local[2]").setAppName("sqltest") val sc = new SparkContext(sparkConf)//sqlContext入口 val sqlContext = new org.apache.spark.sql.SQLContext(sc)//RDD导入并解析数据 val StudentData = sc.textFile("hdfs://master:9000/sql_stu.data").map{ lines => val line = lines.split(",") Row(line(0),line(1),line(2),line(3),line(4)) }//把RDD数据和Schema通过sqlContext维护起来形成DataFrame val StudentTable = sqlContext.createDataFrame(StudentData, StudentSchema) StudentTable.registerTempTable("Student")//表名//sql语句使用 sqlContext.sql("SELECT Sname, Ssex, SClass FROM Student").show()2)从hdfs的原始text中读数据(json串):sqlJsonText
    val personInfo = sqlContext.read.json("hdfs://master:9000/person_info.json")//json串中的数据都是维护好的数据,不需要schema personInfo.registerTempTable("personInfo") sqlContext.sql("SELECT id, name, age FROM personInfo").show() println(personInfo.schema)3)从hive中读数据:sqlHiveTest
    启动mysql:]# systemctl start mariadb(hive中元数据)
    终端submit需jar包:lib/mysql-connector-java-5.1.41-bin.jar
    val hiveContext = new HiveContext(sc) hiveContext.table("movie_table").registerTempTable("movie_table")//可对现有表直接进行操作 hiveContext.sql("SELECT movieid, title FROM movie_table").show()2、UDF相关操作:
    1)udf:单条记录处理(map):sqlUdf
    sqlContext.udf.register("strlen", (input: String) => input.length)//函数名字注册,及简单实现功能 val personInfo = sqlContext.read.json("hdfs://master:9000/person_info.json") personInfo.registerTempTable("personInfo") sqlContext.sql("SELECT id, name, strlen(name), age FROM personInfo").show()//字段套用函数2)udaf:聚合场景(groupby)
    例子:每一个打分背后,有多少人参与
    class WordcountUdaf extends UserDefinedAggregateFunction { // 该方法指定具体输入数据的类型 override def inputSchema: StructType = StructType(Array(StructField("input", StringType, true))) //在进行聚合操作的时候所要处理的数据的结果的类型 override def bufferSchema: StructType = StructType(Array(StructField("count", IntegerType, true))) //指定UDAF函数计算后返回的结果类型 override def dataType: DataType = IntegerType // 确保一致性 一般用true override def deterministic: Boolean = true //在Aggregate之前每组数据的初始化结果 override def initialize(buffer: MutableAggregationBuffer): Unit = {buffer(0) =0} // 在进行聚合的时候,每当有新的值进来,对分组后的聚合如何进行计算 // 本地的聚合操作,相当于Hadoop MapReduce模型中的Combiner override def update(buffer: MutableAggregationBuffer, input: Row): Unit = { buffer(0) = buffer.getAs[Int](0) + 1 } //最后在分布式节点进行Local Reduce完成后需要进行全局级别的Merge操作 override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = { buffer1(0) = buffer1.getAs[Int](0) + buffer2.getAs[Int](0) } //返回UDAF最后的计算结果 override def evaluate(buffer: Row): Any = buffer.getAs[Int](0)}val hiveContext = new HiveContext(sc) hiveContext.table("rating_table").registerTempTable("rating_table") hiveContext.udf.register("strlen", (input: String) => input.length) hiveContext.udf.register("wordCount", new WordcountUdaf)//注册 hiveContext.sql("select rating, wordCount(rating) as count, strlen(rating) as length" + " from rating_table group by rating").show()//这里两函数可做个对比3、终端
    /usr/local/src/spark-2.0.2-bin-hadoop2.6/bin/spark-sql \--master local[2] \--jars /usr/local/src/spark-2.0.2-bin-hadoop2.6/lib/mysql-connector-java-5.1.41-bin.jar测试与hive数据:
    select rating, count(*) from rating_table_ex group by rating limit 100;4、streaming+sql:sqlAndStreamingWC
    nc -l 9999//单例模式object SQLContextSingleton { @transient private var instance: SQLContext = _ def getInstance(sparkContext: SparkContext): SQLContext = { if (instance == null) { instance = new SQLContext(sparkContext) } instance }}if (args.length < 2) { System.err.println("Usage: NetworkWordCount <hostname> <port>") System.exit(1) } val sparkConf = new SparkConf().setMaster("local[2]").setAppName("sqlAndStreamingWC") val sc = new SparkContext(sparkConf) val ssc = new StreamingContext(sc, Seconds(30)) val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER) val words = lines.flatMap(_.split(" ")) words.foreachRDD((rdd: RDD[String], time: Time) => { val sqlContext = SQLContextSingleton.getInstance(rdd.sparkContext) import sqlContext.implicits._//每条数据都用一个对象操作内存,复用性 val wordsDataFrame = rdd.map(w => Record(w)).toDF() wordsDataFrame.registerTempTable("words") val wordCountsDataFrame = sqlContext.sql("select word, count(*) as total from words group by word") println(s"========= $time =========") wordCountsDataFrame.show() }) ssc.start() ssc.awaitTermination()5、streaming+sql + hbase:streamSqlHbase
    nc -l 9999object HbaseHandler { def insert(row: String, column: String, value: String) { // Hbase配置 val tableName = "sparkstream_kafkahbase_table" // 定义表名 val hbaseConf = HBaseConfiguration.create() hbaseConf.set("hbase.zookeeper.quorum", "master,slave1,slave2") hbaseConf.set("hbase.zookeeper.property.clientPort", "2181") hbaseConf.set("hbase.defaults.for.version.skip", "true") val hTable = new HTable(hbaseConf, tableName) val thePut = new Put(row.getBytes) thePut.add("info".getBytes,column.getBytes,value.getBytes) hTable.setAutoFlush(false, false) // 写入数据缓存 hTable.setWriteBufferSize(3*1024*1024) hTable.put(thePut) // 提交 hTable.flushCommits() }}//从套接字中读取到的信息遍历解析lines.foreachRDD((rdd: RDD[String], time: Time) => { val sqlContext = SQLContextSingleton.getInstance(rdd.sparkContext) import sqlContext.implicits._ val wordsDataFrame = rdd.map{ x=> (x.split(" ")(0),x.split(" ")(1),x.split(" ")(2)) }.map(w => (w._1, w._2, w._3)).toDF("key", "col", "val") wordsDataFrame.registerTempTable("words") val wordCountsDataFrame = sqlContext.sql("select key, col, val from words") println(s"========= $time =========") wordCountsDataFrame.show() //对dataframe行遍历插入 wordCountsDataFrame.foreach(x => HbaseHandler.insert( x.getAs[String]("key"), x.getAs[String]("col"), x.getAs[String]("val"))) })
    2  留言 2019-04-10 15:19:41
  • 大数据Spark平台5-2、spark-streaming

    前文链接:https://write-bug.com/article/2091.html
    Spark Streaming官网:http://spark.apache.org/docs/latest/streaming-programming-guide.html
    在之前我们已经对Storm流式计算框架和Spark-core核心计算引擎进行了介绍,以此为基础更好理解SparkStreaming。
    Spark Streaming是核心Spark API的扩展,可实现实时数据流的可扩展,高吞吐量,容错流处理。
    对于大多数业务而言,这两种并没有很大差别:

    Storm的数据是类似水流式的流转数据,毫秒级别
    Spark Streaming的数据是类似离散化后的水状数据,秒级别

    数据来源:

    处理后的数据可以推送到文件系统,数据库和实时仪表板。
    在Spark-core中我们主要处理的其实就是不同的RDD算子(Transformation&action),实行懒惰机制。
    在这里我们的action类算子换为了output类算子,其实开发的时候也是有细微的不同,Spark Streaming针对Dstream开发处理结构。Dstream:每个RDD包含特定的时间间隔。
    这里文字说可能不太明白,这一张图就能直接明了。

    Spark Streaming接收实时输入数据流并将数据分成批处理,然后由Spark引擎处理以批量生成最终结果流。

    在这里与其说是对Dstream做开发不如说是把不同的RDD排列组合,再在不同的入口处声明批次秒数生成新的Dstream罢了。(即DStream中的各种操作是可以映射到内部的RDD上运行的)
    在transformation算子中Spark提供了窗口操作,例如:
    统计最近一个小时的PV量,要求每分钟更新。
    参数:

    window length - The duration of the window (3 in the figure).
    sliding interval - The interval at which the window operation is performed (2 in the figure).


    output内分两种算子:

    执行算子:foreachRDD:主要负责对接外部开发,Hbase,Kafka,Hive
    输出算子:print()、saveAsTextFiles(prefix, [suffix])、saveAsObjectFiles(prefix, [suffix])、saveAsHadoopFiles(prefix, [suffix])

    Streaming架构:


    master:分配任务(画Graph蓝图)
    worker:处理任务(接收、发送)
    client:喂数据

    模式:

    Recevier:被动接收数据-异步两线程
    direct:主动拉数据-同步

    容错—WAL:

    数据流从右侧进入,先在内存中顺序存储(有offset偏移量),再同步存储在磁盘文件系统中(如HDFS),executor处理后再把存储的元数据发送给AM,AM得到请求后并且已知数据存储结构就可以通过SSC入口处理数据,处理数据之前根据元数据把内存中的数据先同步过来,处理数据时可能有数据落地需求;与此同时,元数据结构和数据checkpoint形式存储在文件系统中比便数据恢复。
    重启恢复:

    实践:
    1.word count:
    无状态:不记录上一批次数据
    //定义入口,初始化配置val sparkConf = new SparkConf().setMaster("local[2]").setAppName("wordCount")val ssc = new StreamingContext(sparkConf, Seconds(5))//和之前的Spark-core入口不同,并且参数多了个批次时间设置ssc.checkpoint("hdfs://master:9000/hdfs_checkpoint")//可设置checkpoint点以便恢复数据在context被初始化后,你还需要做如下几点:

    通过input DStream来定义输入源
    通过DStream的转换操作和输出操作来定义流数据处理规则
    开始接受数据并处理:ssc.start()
    等待流处理终止(或者出现异常退出):ssc.awaitTermination()
    手动终止流处理:ssc.stop()

    //这里从一个TCP数据源接收流式数据,在这里我们需要指定主机和端口。还指定了存储等级:内存、磁盘、序列val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER)//处理数据逻辑后想要真正开始启动任务调用的方法 ssc.start() ssc.awaitTermination()我们用netcat模拟服务器发送数据:
    //监听模式向9999端口发送数据nc -l 9999有状态的:记录上批次数据并累加
    //改变原来的reducebykey为updateStateByKey(updateFunction _)//updateFunction为自己开发的函数,即把前面批次数据和后面批次数据相加def updateFunction(currentValues: Seq[Int], preValues: Option[Int]): Option[Int] = {//Scala Option(选项)类型用来表示一个值是可选的(有值或无值)。//Option[T] 是一个类型为 T 的可选值的容器: 如果值存在, Option[T] 就是一个 Some[T] 即成功返回T类型,如果不存在, Option[T] 就是对象 None 。 val current = currentValues.sum val pre = preValues.getOrElse(0)// getOrElse() 方法来获取元组中存在的元素或者使用其默认的值 Some(current + pre)//Scala数据类型 }2.时间窗口
    这里只需要把我们的reducebykey算子改为:
    reduceByKeyAndWindow((v1: Int, v2:Int) => v1 + v2, Seconds(30), Seconds(10))每10秒更新一次数据,更新最近30秒钟的结果,后面10秒参数要和前面设置的批次时间相同;如果批次时间小于10秒,则更新数据时间和批次数据无关,如果大于10秒,则无论时间窗口怎样更新数据,都不会显示。
    3.Kafka+Streaming -wordcount
    conf 加:set(“spark.cores.max”, “8”)
    Recevier模式:
    如果改为local【1】个线程,将不会正常工作出结果。
    在有状态的基础上添加:
    //ReceiverInputDStream类型val zkQuorum = "master:2181,slave1:2181,slave2:2181"val groupId = "group_1"val topicAndLine: ReceiverInputDStream[(String, String)] = KafkaUtils.createStream(ssc, zkQuorum, groupId, Map("topic_1013" -> 1), StorageLevel.MEMORY_AND_DISK_SER) val lines: DStream[String] = topicAndLine.map{ x => x._2 }KafkaUtils.createDstream
    构造函数为KafkaUtils.createDstream(ssc, [zk], [consumer group id], [per-topic,partitions] )
    使用了receivers来接收数据,利用的是Kafka高层次的消费者api,对于所有的receivers接收到的数据将会保存在spark executors中,然后通过Spark Streaming启动job来处理这些数据,默认会丢失,可启用WAL日志,该日志存储在HDFS上

    创建一个receiver来对kafka进行定时拉取数据,ssc的rdd分区和kafka的topic分区不是一个概念,故如果增加特定主体分区数仅仅是增加一个receiver中消费topic的线程数,并不增加spark的并行处理数据数量
    对于不同的group和topic可以使用多个receivers创建不同的DStream
    如果启用了WAL,需要设置存储级别,即KafkaUtils.createStream(….,StorageLevel.MEMORY_AND_DISK_SER)

    Direct模式:
    val brokers = "192.168.88.101:9092"; val topics = "topic_1013"; val topicSet = topics.split(",").toSet val kafkaParams = Map[String, String]("metadata.broker.list" -> brokers) val messages = KafkaUtils.createDirectStream[String, String, StringDecoder, StringDecoder](ssc, kafkaParams, topicSet)KafkaUtils.createDirectStream
    区别Receiver接收数据,这种方式定期地从kafka的topic+partition中查询最新的偏移量,再根据偏移量范围在每个batch里面处理数据,使用的是kafka的简单消费者api
    优点:

    简化并行,不需要多个kafka输入流,该方法将会创建和kafka分区一样的rdd个数,而且会从kafka并行读取。
    高效,这种方式并不需要WAL,WAL模式需要对数据复制两次,第一次是被kafka复制,另一次是写到wal中
    恰好一次语义(Exactly-once-semantics),传统的读取kafka数据是通过kafka高层次api把偏移量写入zookeeper中,存在数据丢失的可能性是zookeeper中和ssc的偏移量不一致。EOS通过实现kafka低层次api,偏移量仅仅被ssc保存在checkpoint中,消除了zk和ssc偏移量不一致的问题。缺点是无法使用基于zookeeper的kafka监控工具

    数据挤压问题:
    数据挤压:下游处理速度慢(并发不够、处理速度慢)
    kafka -> streaming

    数据分布,调节offset——紧急
    并发调大,需要kafka配合(增加分区数),提高线程数量
    控制批次的规模—— max.poll.records
    控制数据处理时间(timeout)—— max.poll.interval.ms

    4.Kafka+Streaming+Kafka
    上面对接数据后,这里后面要对接外部服务,用到了前面说的执行算子foreachRdd:
    val array = ArrayBuffer[String]() lines.foreachRDD(rdd => {//遍历每批次数据 val count = rdd.count().toInt rdd.take(count + 1).take(count).foreach(x => {//遍历每条数据 array += x + "--read" }) ProducerSender(array) array.clear() })自己为producer发送数据:
    def ProducerSender(args: ArrayBuffer[String]): Unit = { if (args != null) { val brokers = "192.168.88.101:9092" // Zookeeper connection properties val props = new HashMap[String, Object]() props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers) props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer") props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer") val producer = new KafkaProducer[String, String](props) val topic = "topic_1013"//可输出到不同的topic,相同会有好玩现象 // Send some messages for (arg <- args) { println("i have send message: " + arg) val message = new ProducerRecord[String, String](topic, null, arg) producer.send(message) } Thread.sleep(500) producer.close() }5.Kafka+Streaming+HbaseHBase 配置:
    object HbaseHandler { def insert(row: String, column: String, value: String) { // Hbase配置 val tableName = "sparkstream_kafkahbase_table" // 定义表名 val hbaseConf = HBaseConfiguration.create() hbaseConf.set("hbase.zookeeper.quorum", "master,slave1,slave2") hbaseConf.set("hbase.zookeeper.property.clientPort", "2181") hbaseConf.set("hbase.defaults.for.version.skip", "true") val hTable = new HTable(hbaseConf, tableName) val thePut = new Put(row.getBytes) thePut.add("info".getBytes,column.getBytes,value.getBytes) hTable.setAutoFlush(false, false) // 写入数据缓存 hTable.setWriteBufferSize(3*1024*1024) hTable.put(thePut) // 提交 hTable.flushCommits() }}val topicpMap = topics.split(",").map((_,numThreads.toInt)).toMap val lines = KafkaUtils.createStream(ssc, zkQuorum, group, topicpMap).map(_._2) val line = lines.flatMap(_.split("\n")) val words = line.map(_.split("\\|")) words.foreachRDD(rdd => {//遍历批次数据 rdd.foreachPartition(partitionOfRecords => {//遍历Kafka分区数据 partitionOfRecords.foreach(pair => {//遍历每条记录 val key = pair(0) val col = pair(1) val value = pair(2) println(key + "_" + col + " : " + value) HbaseHandler.insert(key, col, value) }) }) })运行模式:

    idea中:注意pom中指定好版本文件
    Linux终端中:/bin/spark-submit —master local[2] (代码中也可指定), —class classname jar-path IP portLinux文件重定向:bash run.sh 1>1.log 2>2.log 便于查看数据
    Linux终端中Standard: —master spark: //master: 7077
    Linux终端中yarn模式: —master yarn-cluster

    后两种可指定参数:
    --num-executors 2 \--executor-memory 1g \--executor-cores 2 \--driver-memory 1g \
    2  留言 2019-04-08 17:49:12
  • 大数据Storm平台11、Storm

    前文链接:https://write-bug.com/article/2229.html
    Storm在前文中我们说过了离线数据挖掘的MR和Spark-core,而Storm是通过流式处理实时数据的计算框架。
    MR中

    处理海量量数据,吞吐能力强
    一次性处理整个数据集
    大批量输入大批量输出
    中间磁盘落地dump
    任务执行完结束

    Storm中

    实时分析数据(实时报表动态、流量波动、反馈系统)
    时效性(毫秒级)
    增量式处理
    全程在内存种流转
    任务无结束


    在MR中,我们输入数据都是一次性把文件直接塞给处理程序,之后MR会自动分片数据分为多少个任务去处理,而在Storm中,实行增量式处理,就是来一条数据处理一条数据(类似生产线源源不断产生和输入),可以做做公司的实时报表、监控流量、迭代数据,用输出数据来指导输入数据、去做反馈系统。

    在提到Storm时,有个流式处理的概念,而实际上刚才说过就像工厂罐头传送带一样处理并输出,在我们学MR时,有个本地调试命令:
    cat input|python map.py|sort|python reduce.py >> output
    就像一个管道似的被接连处理。
    在分布式流式计算这样的大环境中,Storm只是其中一种实现方案,后面会有Spark-Streaming(分钟级别)等。而分布式需要解决什么?流量控制、容灾冗余、路径选择、拓展。
    Storm特点

    无持久化层—>速度快,中间无dump,整个过程在内存中进行。
    保证消息得到处理—>可靠性
    本地模式—>模拟集群功能
    支持多种编程语言—>thrift RPC协议实现
    0.8版本之前ZeroMQ做底层消息队列—>高效
    原语spout/bolt—>类似map/reduce

    Storm基本概念

    框代表同一角色,一个圈是一个spout task/bolt task,四个圈是四个并发度。

    Stream:以tuple给基本单位形成的有向无界数据流
    Tuple:最基本的数据单元,包裹内部数据类型——对应HDFS的block
    Topology:由spout和bolt组成的网络拓扑图,类似MR中的Job,但不会结束,除非主动Kill

    任务提交:Storm jar code.jar MyTopology(类) arg1(参数) arg2storm jar 负责来连接Nimbus(相当于MR的Job Tracker)并上传jar包stream-Grouping等方式连接:Shuffle/Fields
    Spout:消息生产者(水龙头)

    可以对接很多类型的数据流收集消息处理的ack、fail,那成功和失败了后怎么处理还需要自己开发,所以如果消息失败,可重新emit一个tuple,后续处理可自行开发可指定emit多个Stream流:OutFieldsDeclarer.declareStream定义,SpoutOutputCollector指定(随机)nextTuple 开发时主要函数
    Bolt:消息处理逻辑

    过滤、访问外部服务(数据库等)、数据格式化、聚合、汇总。。。多bolt处理负责步骤execute 主方法

    在业务角度看:
    MR+Storm结合使用

    在某种场景下,如果算法足够简单或者MR和Storm的算法可以达到同样效果情况下,可以用Storm做整条数据处理,但是最好用MR去对以星期/月为基准做一个处理保障,毕竟MR更稳定并且有中途存储机制,而Storm一次处理的数据量又有限,如果算法比较复杂,就只能用Storm尽量拟合结果做输出,之后MR再以天为基准做处理并存储.
    如果Storm运行中突然挂掉,那处理方式肯定是报警人工重启了,但是在挂掉到重启的这段时间的数据肯定是丢了,如果不找回来会影响我后面计算的准确度,那我如何回溯这部分数据呢,一种是去上游(比如说是Kafka消息队列)回溯消息,另一种是借助存储机制(比如说Storm后面可以连接Hbase)可以通过Hbase的时间戳一判断就可以回溯到这部分数据.再不济情况下反正我Storm对结果的拟合度并不高,那只是今天有影响,第二天我就可以通过MR追回来数据的效果,根据业务来考虑.
    Stream-Grouping

    shuffle-grouping:随机分组,负载均衡
    fields-grouping:按制定的field(相当于key,可开发)分组
    all-grouping:广播分组
    Global-grouping:全局分组(类似合并)

    Storm应用场景
    常见模式:

    流式
    过滤:bolt逻辑组装(多到一)


    此图主要展示组装概念,当然也可以像下面的图一样组装


    Join(多到一)这里的join不像MR中的全局join一样保证数据,因为毕竟数据不完整所以业务用的很少
    持续计算——机器学习迭代用bolt计算的输出来指导spout的输入,迭代计算
    分布式RPC——独立服务比如在用户所请求的服务器上设置redis数据库,通过请求数据计算并输出到个性化数据存储到redis有延时性.


    Storm架构


    主:Nimbus:分配任务和资源调度如果挂掉:因为本身不存储并且在内存中流转,重启之后,像什么事情没有发生一样——无状态(快速失败fail-fast)
    从:Supervisor:监控工作接收任务快速失败fail-fast,监控自己的Worker工作
    Worker:工作进程,内存有多个executor
    executor线程池,里面维护很多task(轮转执行),默认每次只会执行一个task(可配置)
    Task:线程,Storm最细的粒度,本质是一个节点类的实例对象spout和bolt的线程都是task
    Zookeeper协调管理,所有状态数据在zookeeper上或本地磁盘上

    通过几句代码可以直接构建一幅拓扑图:
    //对象内部有默认配置,可修改 Config conf = new Config(); //设置Worker数量 conf.setNumWorkers(2); // 设置Executor数量 topolopyBuilder.setSpout("BlueSpout", new BlueSpout(), 2); topolopyBuilder.setBolt("GreenBolt", new GreenBolt(), 2) .setNumTasks(4) // 设置Task数量 .shuffleGrouping("BlueSpout"); topolopyBuilder.setBolt("YellowBolt", new YellowBolt(), 6) .shuffleGrouping("GreenBolt");
    在一次任务中,可能一个executor有多个task,而且数目一旦生成除非改代码否则不能动态改变,那我们可以动态调配这次任务的并发度

    重新配置Topology “myTopology”使用5个Workers
    BlueSpout使用3个Executors
    Yellowbolt使用10个Executors

    ]# storm rebalance myTopology -n 5 -e BlueSpout=3 -e YellowBolt=10

    strom容错
    架构容错:

    Zookeeper

    存储Nimbus与Supervisor数据
    节点宕机

    Heartbeatworker汇报executor信息supervisor汇报自身信息通过zookeeperNimbus
    Nimbus/Supervisor宕机

    Worker继续工作Worker失败,任务失败
    Worker出错
    Supervisor重启worker

    数据容错:

    timeout

    防止集群阻塞
    ack机制

    ack本质是一个或多个task,特殊的task,非常轻量,工作:反馈信息和透传每个Topology都有一个Acker组件
    所有节点ack成功,任务成功

    实现:tupleid和ack的tupleid作异或=0表示成功




    一个tuple没有ack

    处理的task挂掉了,超时,重新处理
    Ack挂掉了

    一致性hash 全挂了,超时,重新处理



    Spout挂掉了
    重新处理

    Bolt

    Anchoring 产生新tuple

    将tuple作为一个锚点添加到原tuple上
    Multi-anchoring

    如果tuple有两个原tuple,则为每个tuple添加一个锚点
    Ack

    通知ack task,该tuple已被当前bolt成功消费
    Fail

    通知ack task,该tuple已被消费失败

    Storm开发
    Spout
    public void nextTuple() { Utils.sleep(100); final String[] words = new String[] {"nathan", "mike", "jackson", "golda", "bertels"}; final Random rand = new Random(); final String word = words[rand.nextInt(words.length)]; _collector.emit(new Values(word)); }Bolt
    public static class ExclamationBolt implements IRichBolt { OutputCollector _collector; public void prepare(Map conf, TopologyContext context, OutputCollector collector) { _collector = collector; } public void execute(Tuple tuple) { _collector.emit(tuple, new Values(tuple.getString(0) + "!!!")); _collector.ack(tuple); } public void cleanup() { } public void declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare(new Fields("word")); } }本地模式与conf常用配置
    本地模式运行ExclamationTopology的代码: Config conf = new Config(); conf.setDebug(true); //conf.setNumWorkers(2); LocalCluster cluster = new LocalCluster(); cluster.submitTopology("test", conf, builder.createTopology());• Config.TOPOLOGY_WORKERS• Config.TOPOLOGY_ACKERS• Config.TOPOLOGY_MAX_SPOUT_PENDING• Config.TOPOLOGY_MESSAGE_TIMEOUT_SECS
    实践:

    Storm-word count
    Flume+kafka+Storm+Hbase+Hive —— 数据通路
    实时推荐:Storm+中文分词
    2  留言 2019-03-07 15:15:34
  • 大数据日志系统10、Kafka

    前文链接:https://write-bug.com/article/2127.html
    Kafka我们前面刚学习了flume,他是一个海量日志收集系统,在生态系统中的定位为前端采集系统,通常我们会见到这样的经典组合Flume+Kafka+Storm+Hbase/HDFS,而Kafka在这里就起到一个承上启下的管道作用,定位为:分布式的消息队列系统。

    分布式消息队列系统,基于发布/订阅的系统

    分布式:支持水平拓展和高吞吐能力,像Flume一样可以拓展为很多机器共同协作发布/订阅:一种对外服务的功能,就像微信订阅号一样,发布者在一个话题下发布消息,读者也到这个话题下读取消息并且在读取过程中我们只是在订阅的话题下看到更新才去读取,但这个话题下的消息有可能是昨天发布的,所以我们并不是同步读取的,而是通过异步的方式,而这个话题就像一个缓冲器,暂时存储了发布者发布的信息,之后我们订阅者再从这里异步读取信息
    消息持久化:Kafka的一个特殊点,数据直接向磁盘中写,那我们可能会说其他的框架为了增加速度都是先把数据往内存中写再往磁盘中写,Kafka会不会很慢呢?这里Kafka做了大量优化,我们操作系统有两个特点:预读和后写

    预读:预先读取下一行后写:把多次写压缩成一次写内存快的特点:随机访问读写,学过数据结构的人都知道,最开始的数组可以通过顺序存储的方式,下标访问会更快速读写数据,虽然会占用一部分空间,但是比起大数据来说都是毛毛雨了,而Kafka对持久化优化的方式就是顺序存储,对磁盘的顺序访问,可以做大O(1)的方式进行数据处理(比内存访问还要快)
    实时性:生产者生产的message立即被消费者可见

    message:Kafka的数据单位,可对比HDFS的block,Flume中的event

    在之前我们要做一个数据统计和报表,我们是通过离线的方式用Hbase和Hive去做一个数据统计分析,但他的实时性太差了,维护离线数据的成本也很高,基于这一点,我们现有的消息队列系统都把目光放在了实时上,没有注重消息的持久化,那么我们需要一个既支持在线又支持离线的系统就是Kafka。
    组件
    Broker:每一台Kafka机器节点是一个Broker
    Producer:日志(即message)消息生产者,主要写数据
    Consumer:日志消息消费者,主要读数据



    Topic:是虚拟概念(前面说的话题),不同的consumer去指定的topic去读数据,不同producer可以往不同的topic去写
    Partition:是实际概念,文件夹形式存在,是在Topic的基础上做了进一步分层,有些类似MR中的Partition,
    Partition功能:负载均衡,需要保证消息的顺序性

    顺序性的保证:订阅消息是从头往后读取的,写消息是尾部追加,所以整体消息是顺序的如果有多个partiton存在,可能会出现顺序不一致的情况,原因:每个Partition相互独立
    Partition有两部分组成:

    (1)index log:(存储索引信息,快速定位segment文件)(2)message log:(真实数据的所在)

    流程:
    Producer发布一个消息,并绑定一个Topic,Topic的物理概念是Partition文件夹来存储,而partition存在了不同的Broker中(hash%),从而Topic也就分布在了不同的Broker中(负载均衡)。
    当Broker的管道建好后,Producer可以进行写,Consumer进行读操作,这个读和写是同时进行的,当然这其中有一定延迟,而对于Broker来说,Producer和Consumer都是客户端,也就是说我读和写都是不需要顾及其他人的,只负责自己的功能,也就是Kafka对两端做异步解耦。
    在HDFS多副本的方式来完成数据高可用,如果设置一个Topic,假设这个Topic有5个Partition,3个replication。
    Kafka分配replication的算法:
    假设:
    将第i个Partition分配到(i % N)个Broker上
    将第i个Partition的第j个replication分配到( (i+j) % N)个Broker上
    虽然Partition里面有多个replication
    如果里面有M个replication,其中有一个是Leader,其他M-1个follower

    在Kafka集群中,和Strom/Hbase相似,也依赖了一个Zookeeper组件,在上图可以看出,zookeeper中存储了一些Broker和Consumer的信息,而Producer发布消息是要指定topic(Broker)的,不需要zookeeper去做一个分发,对于Consumer来说,需要用zookeeper去做一个负载均衡,比如说我有多个consumer,那我们把它用组的方式维护起来,这个Group组对于kafka来说就是一个逻辑概念,这时上游来个一个数据,如果我把这个数据每台真实consumer都存储一次这个数据可能对存储性能有很大的要求,那么这里我们就可以像Hive分库一样,每一个子节点存储一部分数据,可以做并行计算。
    同样的(id%hash)去分配数据做负载均衡。producer和broker之间是一个push模式,然后Consumer和producer之间是一个pull模式,push模式就是推,pull模式就是拉,那不管你是push模式还是pull模式都是由你的producer和Consumer进行主动的,就像producer有一个消息,我主动的把消息推给你的broker,然后Consumer我要消费了,那我主动的去在你的broker去取数据
    topic角度来看,上面说过topic相当于话题,也就是消息的类别,在物理上topic的数据分开在不同的broker上存储,而用户只需要指定消息的topic就可以了,而不需要关心消息存储到那台机器中,topic是一个逻辑概念,消息传递有一个基于时间顺序的先后顺序,那么就算分配到不同子节点中,也是局部有序(partition)的,这里有一个offset偏移量,记录着Consumer读取数据时的地址后移动的取值范围,类似文章的第几行,这种topic划分多个partition的方式可以有效地提高Kafka的吞吐率,而对于topic来说,无论消息有没有被消费,消息都会一直持久化,存储的时间默认是7天,以便后续下游出错可以回溯,下游的消费进度是不同的,而读取到哪里这个标记就是偏移量offset,考虑到Kafka性能的原因,把主导权给了consumer去维护,也就是自己保存自己的读取位置。
    Message:前面说过其为数据传递单位,producer生产消息追加在文件末尾,一旦追加后就不能改变,其消息格式为:
    message length:4bytes(1+4+n)“magic”value: 1bytecrc: 4bytespayload: 真实数据,nbytes
    Producer:在Kafka种有现成的demo可供使用,当然也可以直接用flume对接Kafka,这样flume就是一个producer。
    producer分为两种模式:
    producer.type=sync 同步 实时发送
    producer.type=async异步 数据来了后现在Kafka中暂存一部分,当达到一定条件,数据在被应用:

    时间
    数据量

    Consumer:同样在前面我们提到了组概念,即消费组(Consumer Group),这种更抽象的消费方式把消息分片成了几个部分存在各自的Consumer中,在搭建时,可设置多个消费组,而不管Kafka内部的几个partition对应了几个Group中的Consumer,对于Group全局来说我得到数据都是完整的。
    随着Producer的写入,Kafka内存文件不断增大,那么在这样的大文件中我怎么快速查找到我想要的数据呢,kafka内部是实际上是这样做的,它是用多个segment去把这个大的segment给它拆分,这样的话比如说我先写1号segment,写完之后再写2号segment,2号写完了写3号,所以你会发现一个特点,这个1号segment,2号segment,3号segment当然它还在不断的产生数据,而且产生的时候这个segment这个文件大小基本上也都是一致的,那随着这个数据的不断积累,那你这个segment也是不断的增多是吧?但是你越旧的数据或者越早的数据,是存到了前面数字越小的segment里面去,越新的数据就存在了这个数字的越大的segment这里面去。当我查历史数据时,我知道offset,但不知道具体在哪个segment上,这里基于Kafka的顺序存储,我们可以用二分法快速定位segment。

    传输效率:zero-copy。
    0拷贝:减少Kernel和User模式上下文的切换。直接把disk上的data传输给socket,而不是通过应用程序来传输。
    Kafka的消息是无状态的,消费者必须自己维护已消费的状态信息(offset);减轻Kafka的实现难度。
    Kafka内部有一个时间策略:SLA——消息保留策略(消息超过一定时间后,会自动删除)
    交付保证:

    at least once:至少一次(会有重复、但不丢失)at most once:最多发送一次(不重复、但可能丢失)exactly once:只有一次(最理想),目前不支持,只能靠客户端维护
    Kafka集群里面,topic内部由多个partition(包含多个replication),达到高可用的目的:

    日志副本:保证可靠性角色:主、从ISR:是一个集合,只有在集合中的follower,才有机会被选为leader如何让leader知道follower是否成功接收数据(心跳,ack)如果心跳正常,代表节点活着
    怎么算“活着”

    心跳如果follower能够紧随leader的更新,不至于被落的太远如果一旦挂掉,从ISR集合把该节点删除掉
    实践(需要把zookeeper提前启动好):
    一、单机版
    1、启动进程:
    ]# ./bin/kafka-server-start.sh config/server.properties2、查看topic列表:
    ]# ./bin/kafka-topics.sh --list --zookeeper localhost:21813、创建topic:
    ]# ./bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic newyear_test4、查看topic描述:
    ]# ./bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic newyear_test5、producer发送数据:
    ]# ./bin/kafka-console-producer.sh --broker-list localhost:9092 --topic newyear_test6、consumer接收数据:
    ]# ./bin/kafka-console-consumer.sh --zookeeper localhost:2181 --topic newyear_test --from-beginning7、删除topic:
    ]# ./bin/kafka-topics.sh --delete --zookeeper localhost:2181 --topic newyear_test二、集群版
    在slave1和slave2上的broker.id一定设置不同,分别在slave1和slave2上开启进程:
    ./bin/kafka-server-start.sh config/server.properties创建topic:
    ]# ./bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 5 --topic newyear_many_test下面命令会创建失败:原因副本数超出实际机器个数
    ]# ./bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 4 --partitions 5 --topic newyear_many_test_2三、自主写producer、consumer
    1、实现一个consumer group
    首先在不同的终端分别开启consumer,保证groupid一致
    ]# python consumer_kafka.py]# python consumer_kafka.py执行一次producer:
    ]# python producer_kafka.py2、指定partition发送数据
    ]# python producer_kafka_2.py3、指定partition读出数据
    ]# python consumer_kafka_2.py四、Flume+Kafka
    1、启动flume:
    ]# ./bin/flume-ng agent --conf conf --conf-file ./conf/flume_kafka_2.conf --name a1 -Dflume.root.logger=INFO,console发送:
    ]# for i in `seq 1 100`; do echo '====> '$i >> 1.log ; done ]# curl -X POST -d '[{"headers":{"flume":"flume is very easy!"}, "body":"111"}]' http://master:52020]# cat conf/flume_kafka_2.conf # Name the components on this agenta1.sources = r1a1.sinks = k1a1.channels = c1# Describe/configure the source#a1.sources.r1.type = exec#a1.sources.r1.command = tail -f /root/9_codes/flume_test/1.loga1.sources.r1.type = httpa1.sources.r1.host = mastera1.sources.r1.port = 52020a1.sources.r1.interceptors = i1a1.sources.r1.interceptors.i1.type = org.apache.flume.sink.solr.morphline.UUIDInterceptor$Buildera1.sources.r1.interceptors.i1.headerName = keya1.sources.r1.interceptors.i1.preserveExisting = false# Describe the sinka1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSinka1.sinks.k1.brokerList = master:9092#a1.sinks.k1.topic = badou_flume_kafka_test#a1.sinks.k1.topic = badou_storm_kafka_testa1.sinks.k1.topic = newyear_many_test# Use a channel which buffers events in memorya1.channels.c1.type = memorya1.channels.c1.capacity = 1000a1.channels.c1.transactionCapacity = 100# Bind the source and sink to the channela1.sources.r1.channels = c1a1.sinks.k1.channel = c1
    2  留言 2019-03-04 15:05:37
  • 大数据日志系统9、flume

    前文链接:https://write-bug.com/article/2123.html
    Flume日志收集系统
    Apache Flume是一个分布式、可信任的弹性系统,用于高效收集、汇聚和移动 大规模日志信息从多种不同的数据源到一个集中的数据存储中心(HDFS、 HBase)
    Flume它是一个消息采集系统,什么是消息采集呢?
    消息就是说你的数据源也就是你的消息源,在这个用户他会通过一系列行为他会留下大量的行为数据或者是行为消息,那这些消息都是更接近于更原始的最原生没有任何过滤的一些有价值的信息提取,相当于是整个的一个记录序列里面,它既有价值信息又有参杂的一些过渡修饰的一些结构,需要被过滤的一些消息,那这个时候你需要把这些大量的消息从数据源开始进行一个收集,因为用户在去留下它们的日志行为的时候,其实这些行为都会被留在了或者被散落在了各个不同的服务器的一个角落,那相当于这些服务器也就是散落在不同机房不同地域的一些各个的数据节点上或者一个服务器节点上,那这个时候它这个数据是非常一个散落的状态,这个时候就需要一个服务,把这些散落的这些原始日志进行一个统一的一个收集,然后供后续的整个的流程比如什么数据过滤,数据入库,数据挖掘等等这些后续我们有待于去进一步操作的事情,所以第一步首先想办法怎么把这个最原始的数据先对接过来,那这个时候就需要用类似于Flume这样的一个消息采集系统到目前为止我们在之前的学习过程中已经完成了很多的一些重要的一些组件,那不同的组件其实有各自的特点然后每一个组件都适合不同的场景,那其实我们在之前不管学hive也好学hbase也好或者是学hdfs,那你会发现这么多个组件其实在整个的这个架构里面它们都处于一个完整项目的一个中下游,中下游就是相当于说是消费者这么一个状态,那起码你的有生产者,生产者你不可能消息一开始就生产到了HDFS上对吧?
    然后这个时候你需要通过一个中间介然后把最原始的消息采集过来然后再去传到后面不管是HDFS上还是hbase这些等等的存储上面去,这个时候相当于我们之前学的不管是HDFS,hive,hbase都是属于下游的一个角色,而且我们还学了一些集成框架,这框架有mapreduce,storm以及spark都是来解决数据计算的一些问题,然后hive,hbase主要是解决一个数据存储和结构化的问题,所以这个时候我们既然已经学了这么多处于一个从消费者的状态这么一个角色一些个组件的话,我们就要想我们这些数据的源头是哪里对不对?那很多时候我们做项目的时候这数据已经给你好现成的了,你就直接去做处理就可以了,你不需要关心这个数据源在哪里,但是你从一个完整的一个项目的宏观角度去观察,你必须要知道这个数据它的来源是在哪里是不是?所以为了保证整个的项目的完整性,保证你对整个的一个数据流的一个打通一个鼻环的一个认识,就是你的数据采集这一块也是有必要掌握的对吧?所以数据采集这一块我们通常就用这个flume方式进行一个消息采集
    数据源:

    server Log(tail、grep查看):webserver
    远程调用:http接口(url)、RPC
    网络:netcat:IP:port(生产消费)
    文件系统:目录树数据变化
    终端:Console
    文本:Text
    数据库:Exec

    你有数据采集之后那你接下来就假设把这个原始信息拿到了,那么就需要把这个数据做一个缓存,先把这个消息进行存储起来,然后存储起来之后因为你这个消息会存在着大量的无效的一些信息,你需要做一些有效字段或者有效结构化一些提取,这时候就涉及到了数据过滤环节。
    跟着这样的一个思路,从数据源开始通过一个服务把数据采集下来,采集的数据需要通过某一个存储或者是某一个缓存把它暂时的存储起来,起码这个数据先落地,落地到具体哪个位置的话这个先不用考虑,起码先把这个数据拿下来,那下来这个数据因为比较原始所以你需要对这个数据进行过滤,因为这数据存在大量的无效的数据。
    然后接下来过滤完之后你需要做一些个转换工作,比如说你这个数据是从一个非结构化的数据变成一个结构化的数据是吧?你怎么把这个数据从你的文件系统里面怎么样转换到一个像数据表格那样,字段然后记录行列之间非常清晰分布这么一个状态,这样有便于后续的一些分析,那转换后的数据就很明显了,需要把它进行一些存储,比如说把它存到HDFS上或者存在你的hbase上等,好了你把这个数据存储完之后那接下来需要做一些检索工作,就需要用于一些检索用,那检索相信你这个数据存储的话你是肯定是要后面来用的对吧?那怎么让下游用的更方便或者是更快捷?这里面就肯定涉及到了一些键索引,那你比如说像之前搞过mysql的人,然后为了让你的数据检索的更快那它自身会支持一些个索引。
    那在整个的大数据里面比如说这个HDFS,比如存储在hbase上那这个时候估计大家就会有疑问了,这个索引怎么建呢?那这个时候其实跟我们后面要学的数据挖掘部分怎么去做一个数据分析还是有一定的关联的。Hbase这块它也是支持一定的索引的。
    那不管怎么说就目的就是能够快速的检索你的数据,那检索到数据之后就开始做一些个数据分析,然后分析后的数据就是把你有用的信息怎么去挖掘出来,然后把挖掘这最后的数据进行一些服务,大概是这么几个环节(如下图所示)

    那整个的一个数据的一个走向那从上游慢慢从水游一样游到了下游,那你会发现数据采集这一块是非常非常贴近于最源头了是吧?那所以今天我们就要了解数据采集这块,那有很多人不太理解为什么你今天非要讲flume,为什么不讲kafka,那有基础的同学那在上图1中里面应该是处于哪个模块?Kafka是一个消息缓冲器对吧?那么相当于kafka是相当于上图1中的缓存器,那么我们学完数据采集再去学kafka,像这样的话你会发现会整个的思路来说会比较更顺畅一些是吧?我们先学上游再去学下游这么一个思路,那么这个过滤是需要做什么呢?这个过滤结合我们之前的内容来说,我们这个过滤如果要做你想怎么做?你可以用mapreduce用storm等去实现,我们之后会有一个案例,怎么把flume和kafka和Storm关联起来对不对?相当于整个链路就打通了。那转换这一块就好说了,不就是转成hive或者是hbase。
    从最开始的图我们就可以看出Flume在其中是一个承上启下的角色,左边流入,右边流出,而且你后面的分析,数据挖掘都是在你的统一的这套存储系统上去做的,主要是想办法怎么去让日志消息收集过来,就是这么一个作用。除了上面多种方式高效接入接出数据的特点,Flume还支持多路径流量,多管道接入流量,多管道接出流量,上下文路由等特点还可以被水平扩展。那么这几个特点是什么意思呢?
    比如说多个log服务器和一个flume和两个存储系统,好了那我这个日志就可以通过flume集群,其实这个flume只画了一个模块,其实这个flume你可以搭建一个集群的方式通过多个不同的机器来维护整套的消息采集这么一个系统,因为这个消息量还是很大的,你只要通过一台机器的话,那通常是搞不定,所以flume还是通过一系列集群来去并发的收集信息。
    然后flume的这个数据也可以进行一个多路输出,意思就是你可以把一个消息可以选择性的去存储到1还是2中,比如说你这个存储1,你这个存储1后面走的挖掘策略就不一样了。这存储2就是另外一套挖掘策略方案(如下图所示)

    而且这两个日志可能不是完全一样的,比如说这个日志1是来收集展现,这个日志2是来收集点击的,那这个策略1可能就是对展现日志进行一个处理,这个策略2对点击日志进行一个处理,那所以这个时候我们希望展现日志可以通过这么一个管道的方式能够有效的流入存储1里面去,我就不想这log1里面的日志不想流到存储2里面去,那么相当于是flume一旦发现你这个源是来自日志1,那我就可以自动把你这个数据直接放到存储1里面去,并不是说把你的数据能够复制,把同样的一个日志只要你这个下游都是对接到了flume上,那下游所有的节点都会收到同样的一份消息,那flume可以这样有区别的进行对待处理,这是一个它可以指定有效路径的方式,这个方式叫做复用机制。
    还有一个就是复制机制,就是说我不管你这个flume前面这个数据源是什么,只要是你来了一个数据源,只要是我这个存储是属于flume下游,那我所有的存储器我都会接收到同样的一份数据,比如说你这个log1的一条数据进来了,那比如说后面有两个存储,那相当于把你一份的数据我复制成两份,每一份节点我都发一份,这是一个复制机制。
    然后多管道接入流量这块也可以体现出这个问题,就相当于这两个日志有两个管道,那这两个log日志是来自不同的一个日志源级别的,这时候flume就可以通过不同的管道去对一些不同的日志源,然后多管道接出,剩下一个上下文路由,路由刚大概说过了,然后水平扩展这一块因为这个flume通常用的时候也是通过一个集群的方式去用,这个flume你可以进行一些个扩展,比如说你一些节点就是在数据采集过程中不够用了,你可以往上面加一些个节点或者资源然后共同的支撑共同的并发,相当于是可扩展性比较强把。

    那我们先从外部的整个框架入手,那最左边是一个消息的一个发生器(Data Generators),什么是消息发生器呢?就是一个日志服务器,比如说就是你们公司里面那个用户接收请求的它的一些请求信息,然后一些收集的这些服务器,就是一些webserver,那相当于日志发生器就是它已经开始陆续的产生消息了,那后面有一个橙色的一个大框对吧?,这里面就是一个整个的flume,然后flume就是从这个发生器(Data Generators)日志来进行采集,采集之后又得到了后面的一个HDFS或者是hbase存储里面去。
    好了这个时候把这个整个的flume这个黑盒的面纱解开你就可以看到这个flume,里面分成了一个Agent和一个Data Collector,Agent的意思就是一个代理模块,它是用来对消息进行接收和汇集。比如有两个log server1和log server2,那么这个Agent通常是部署到跟你同一个server同一台机器上,你这个og server1是用来不断的产生的日志消息,然后你一旦产生这个消息由这个Agent1这个消息来从你这个server上直接发送出去,那把消息发送给谁呢?就发送给一个叫collector(如下图所示)

    所以你从这图8里面就可以看出我虽然这个flume里面包含了Agent和这个collector,其实通常来说Agent和这个collector是分班部署到不同的节点上的,就是结耦。
    通常来说就是你的Agent会很多,因为你这每一个log server都有一个Agent,那你这个server通常会很多对不对?所以Agent也会很多。那这个时候你后面不是像上图8画的一样,一个Agent和一个collector一一对应并不是这样的,通常来说就是一个Agent可能会对应多个collector,就相当于是你前面有多个Agent的消息统一的被你的collector进行一个收集,一般来说server和Agent是一比一的,好了那Agent把消息发给collector,因为这两个属于不同的机器,这个时候collector会去把真正收集到的信息再去做一个存储(Storage),因为这个存储就HDFS或者是hbase,所以这一块就不需要大家开发了,那你就需要把collector怎么能够通过一个配置的形式把前面消息能够直接发送到指定的相应配置的目标路径上去就可以了,所以通过这个地方大家先了解一下你的Agent和你的collector的定位是怎样的(如下图所示)

    Agent就相当于冲到最前线的,这个collector就相当于是后方基地,然后不断去接收前线的一些消息,然后它在把这个消息怎么再往存储上再去做处理,collector也是可以多个的。
    接下来再看一下我们在去讲storm的时候,storm也类似于这么一个流程是不是?从头往后一个数据流进行一个传输是不是?然后再storm里面它的数据流也是有一定的单位的形式做传输对吧?那这个storm的单位是什么呢?tuple对吧?,那在hdfs上数据的单位是一个block对吧?在flume里面数据单位是Event,是一个事件。假如说在整个的flume里面它内部流转了这些消息都是一个事件,所以flume是用这个Event对象来做一个消息传递的格式(如下图)

    它属于内部数据传输的一个最基本单元,那你把这个事件已经打开,打开它有两个部分组成,那第一个部分就是一个Header,第二个部分就是一个Byte Payload,就是你的头和身体对吧?通常这个数据这个头部你可以有也可以没有这个可以选择的,不一定说这个header就是一定要存在的好吧?那如果说这个header要存在的时候可以理解为一个key,body你把它想象成一个value,如果你把数据有一个key有一个value,大家很容易想到的是在mapreduce里面有一个partition对吧?partition就是用来做分发消息的,也就是说这个Event有两个部分,Header和Byte Payload,这个Header是可有可无,如果是它没有这个Header只有byte Payload的时候,那么byte Payload其实就是存的是数据,那这个时候数据就开始往后流向进行传输是不是?这是一个最直接的流程,但是有的时候你需要对这个数据做一些个路由,最后一些个分发,就是说有的消息我想分发到A节点上但是我不想分发到B节点上,有的消息我想分发到B节点但是我不想分发到A节点上,那对于这种有特殊需求的情况,这个时候你就需要用到header,你必须要分配给它一个key,然后这个时候它做分发key的时候就根据你这个header里面的信息去做一个数据的一个路由,有点类似于分桶,所以相当于把这块跟我们之前学的partition结合起来可能理解起来更容易一些。
    header是key/value形式的,这个其实跟我们说的key和value不属于同一个层次,就是这个header如果你要是有这个信息,这个信息就大概长(k:v,k1: v1)这样的样子,然后你去做一个partition的时候你就根据这个key和value去做一个分发这么一个情况,所以这个时候大家就记住一点我的header就是为了做分发用的,Byte Payload就是存实际的数据内容用的。
    好了这个时候就讲了一个比较重点的东西,就是一个代理(如下图所示)

    这个Agent刚刚我们讲过了,这个flume可以拥有多个Agent,当然也可以拥有一个,然后每一个Agent就是一个进程,这个进程就相当于是在你的服务器上一直运行着然后一直监控着你的这个消息,一直监控着你这个日志的产生,一旦你有日志发生变化了,那这个进程就会把你的消息进行一个数据的收集然后往下游不断的传输。
    如果说你把这个Agent再进行打开,再进行把你的内部细节暴露出来,Agent就可以暴露出三个部分,这三个部分就是source和Channel和sink这三个,那么这三个模块有什么用呢?source就是真正对接你数据源进行输入,而Channel就是一个管道,sink就是一个输出,就是你的source把消息接收过来,然后消息会存到你的管道里面会做一个缓存,缓存我们之前学过一些对吧?这个缓存存储可以是文件形式的,相当于就是在你本地一个落地到磁盘上的那个文件对不对?还有一个就是在你内存分配出一个区域,就是这个数据在你内存中扭转的,不落地这是通过一个memory的形式,所以你的数据输入是可以存在你的文件里面也可以存到你的memory里面,那存到memory里面会更快一点,但是有一个问题就是一旦你这个agent出现了问题那你这个消息因为你存在memory里面,在消息可能会存在丢失的风险。但是为了保证你的消息的可用性可靠性通常建议把消息直接存在你的文件里面,但是这是存到你的文件里还是存到你的内存里面这是你要通过一个配置去配置的。

    source :输入-》对接各种数据源
    channel:缓存管道(file,memory)
    sink :输出-》对接各种存储

    然后输入就是对接各个数据源,输出就是对接各种存储,所以相当于是每一个组件都是各司其职,然后彼此之间能够协同工作,然后让消息能够有效的在内部进行一个扭转。如果要是在Agent里面我们在对各个组件在做一个更深入的了解,那我们接下来看一下source
    source是一个整个的flume的源,它是最贴近于你的消息源的那么一个模块对吧?。它相当于就是一个数据源的外部采集,然后把它外部源数据接收过来然后变化成flume可以识别的格式。这个格式就是一个事件(Event),然后在从这个flume开始内部进行流转。
    然后Channel就是一个通道,刚才我们一直说缓存,你可以把它理解成缓存就好
    通道:采用被动存储的形式,即通道会缓存该事件直到该事件被sink组件处理
    所以Channel是一种短暂的存储容器,它将从source处接收到的event格式的数据缓存 起来,直到它们被sinks消费掉,它在source和sink间起着一共桥梁的作用,channel是一个 完整的事务,这一点保证了数据在收发的时候的一致性. 并且它可以和任意数量的source 和sink链接
    可以通过参数设置event的最大个数
    这时候大家会有一个疑问了,你是一个存储器,那如果要是采集的这个消息量非常大,那一旦超过了你这个缓存的限制,那相当于你的内存就爆掉了对不对?这时候会导致一些节点的风险对吧?会不会有一个数据的累计,然后不断的去膨胀这么一个风险。它是可以通过配置去配置的,就是控制流量,就是控制你这个source从外界接收这个数据每一次接收需要接收多少个事件,它是有一个流量控制的,如果你前面流量放的很足那肯定会对这个存储内存会有一定的压力,那一旦有压力你可以减少这样的一个采集量就可以能够进行一个减缓,这是可以通过一个配置event来进行配置。

    Flume通常选择FileChannel,而不使用Memory Channel
    Memory Channel:内存存储事务,吞吐率极高,但存在丢数据风险
    File Channel:本地磁盘的事务实现模式,保证数据不会丢失(WAL实现)

    另外一个就是消息传到存储这块来之后,那它需要sinks来去对它进行一个消费,所以它这个Channel在这个source和sinks之间搭建了一个桥梁作用。那刚才我们说过了这个Channel它既然是一个存储,那你这个数据可以存到你的FileChannel里面,然后memoryChannel都是把数据存到内存,吞吐力高,效率高,但是容易存在丢数据的风险,那么FileChannel就是需要把你的数据落地了是不是?一旦你这个机器挂了,数据也不会丢失。
    然后sinks相当于就是在整个的Agent里面,sinks就是一个消费者,怎么把这个消息消费掉,那消息是在你的Channel里面,sinks会将你的消息或者你的事件从Channel里面进行,然后并且把你的事件开始往外输出,输出到外部的存储上面去

    Sink会将事件从Channel中移除,并将事件放置到外部数据介质上

    例如:通过Flume HDFS Sink将数据放置到HDFS中,或者放置到下一个Flume的Source,等到 下一个Flume处理。 对于缓存在通道中的事件,Source和Sink采用异步处理的方式
    Sink成功取出Event后,将Event从Channel中移除
    Sink必须作用于一个确切的Channel
    不同类型的Sink:

    存储Event到最终目的的终端:HDFS、Hbase 自动消耗:Null Sink 用于Agent之间通信:Avro

    然后一旦这个消息被sink消费掉之后,这个消息就会从这个Channel里面就会移除,它有点类似于队列的形式,那最后你这个数据是以哪种存储的形式落地了呢?是由sink来决定的,也就是你需要通过配置来控制sink你最终数据是怎么样的方式输出,你的数据是可以存在你的HDFS上或者是hbase上,它没有一个默认的一个输出,这个需要你通过一个很简单的配置你可以来控制这数据是怎么样输出来的,另外一个其实在整个的flume集群里面它是可以允许有类似多个flume,然后进行一个彼此之间的一个关联,这个像flume我们刚才打开过,它主要里面是一个Agent是吧?然后你可以把它当中一个玩具一样,然后进行一个彼此之间的拼装,然后你可以把这个集群做的规模很复杂或者一个很简单都可以,所以它在整个的集群或者是消息采集过程中它的这种集群搭建还是很灵活的。
    比如这个Agent如果你在本地搭建的时候,你这个Agent是可以直接存储到你的存储上的,这是可以的,但是有的时候你的Agent和你这个server是部署到了同一台机器上,那你这个机器就是为了来存储日志的,那你在给这个机器再去开放往这个Storage这个机器上去写的这个权限就不太合适,所以就需要把Agent数据再转到一个统一的一个中心,然后这个机器就可以进行一个对外的写服务(如下图所示)图:

    这个是一种形式,另外一个collector它得把这个分散的数据进行收集,所以通常用的时候就是配了一个agent和一个collector,但是从字面上来看这两个感觉差距很大,其实agent和collector你会发现配置的时候基本上是一样的,只不过是数据源不一样,agent的数据源是来自于你的外边真实的外部数据,你collector来的数据是来自于你的agent,就相当于你来自你的flume组件,其实这个agent和collector本质是一样的,只不过是数据源来源不一样,只不过是为了区分他们的角色,如果说遇到了这种内部组件之间的一个对接就相当于这两个flume之间的一个对接的话,那这个数据通过传输的方式的话就需要通过一个avro的方式进行对接(如下图16)

    你collector要对接的话必须要通过这样的方式去对接,你这个agent这个就很丰富了,对接着这种数据源的格式,数据源的类型就很丰富了。
    刚才我们已经说过了它一个agent内部分为最基本的三个组件是source和channcl和sink,那么这三个组件都是缺一不少的,但是它还有两种组件是可以选项,就是说你可以用可以不用,根据你的业务需求,如果你的业务需求确实是涉及到了这方面的一个要求那你这个组件就应该用,那么source和channcl和sink这三个是必须要有的,还是有两个可选的组件分别是interceptor和一个selector,interceptor是拦截器(如下图所示)


    Interceptor用于Source的一组拦截器,按照预设的顺序必要地方对events进行过滤和自定义的 处理逻辑实现
    在app(应用程序日志)和 source 之间的,对app日志进行拦截处理的。也即在日志进入到 source之前,对日志进行一些包装、清新过滤等等动作

    这个拦截器是在什么位置呢?就是说这个interceptor是在你的数据源和你的source之间的一个环节,那这个环节相当于就是可以对你的数据源提前会做一个过滤,然后这个selector是有点类似于路由选择,就是消息已经在这了,我开始对这个消息进行一个存储,你这个消息不是要存到channel上吗?如果你后面如果有多个channel的话,那这个消息我应该是存到哪个channel上,或者是所有的channel都应该存储这样的消息,这个时候就要配置一个selector,所以selector这块就是像我们最一开始说的复制与复用(如下图所示)

    我们继续看拦截器,拦截器主要是对这个event进行一个过滤或者是自定义一些处理逻辑的实现,它主要是在你这个日志与source之间的,然后对这个日志进行一个拦截,就相当于提前制定哪些日志可以往后传,哪些数据可以直接被丢掉,然后它除了拦截之后它还可以对你的日志数据重新做一些个包装,那主要的提供的一些拦截器就有这么几个

    Timestamp Interceptor:在event的header中添加一个key叫:timestamp,value为当前的时间戳
    Host Interceptor:在event的header中添加一个key叫:host,value为当前机器的hostname或者ip
    Static Interceptor:可以在event的header中添加自定义的key和value
    Regex Filtering Interceptor:通过正则来清洗或包含匹配的events
    Regex Extractor Interceptor:通过正则表达式来在header中添加指定的key,value则为正则匹配的部分

    这些拦截器可以直接互相组合,就是不仅仅通过一个拦截,可以通过多个进行一个拦截器的拼装,通过这个chain的方式进行一个组合起来,然后对于组合之后的话,你可以对它进行一个前后的一个顺序依次的处理。
    然后我们再看一下这个selector,这个selector这块也是容易理解的,刚才我们说过这个selector它有两个事情,一个是复制和复用对吧?那这复制就是分别对外两个配置,一个配置就是这个Replicating,还有一个复用Multiplexing,复制刚刚讲过了,就是一个消息能够被复制多份,复用就是一个消息可以选择性的去选择(如下图)

    channel selectors 有两种类型:

    Replicating Channel Selector (default):将source过来的events发往所有channel
    Multiplexing Channel Selector:而Multiplexing 可以选择该发往哪些channel

    这个source前面有两个不同类型的消息,那这个一个类型的消息你可以选择后面的一个channel,如果只选择某一个channel去做传递消息的话,你可以选择复用的方式,如果这一个消息可以被复制多份,就像一个广播的形式发送消息的话,广播是什么意思呢?广播就是一个消息,被复制出多份然后下游每一个节点都同时接收同样的消息是不是?,那这样的情况就可以用复制的形式去用。
    问题:Multiplexing 需要判断header里指定key的值来决定分发到某个具体的channel,如果demo和demo2同时运行 在同一个服务器上,如果在不同的服务器上运行,我们可以在 source1上加上一个 host 拦截器,这样可以通过header 中的host来判断event该分发给哪个channel,而这里是在同一个服务器上,由host是区分不出来日志的来源的,我们必 须想办法在header中添加一个key来区分日志的来源 – 通过设置上游不同的Source就可以解决
    然后接下来就看一下从整体的角度来看可靠性,那为什么说这个flume它的可靠性还是比较OK的呢?那从这么几点来看

    Flume保证单次跳转可靠性的方式:传送完成后,该事件才会从通道中移除
    Flume使用事务性的方法来保证事件交互的可靠性。
    整个处理过程中,如果因为网络中断或者其他原因,在某一步被迫结束了,这个数据会在下一次重新传输。
    Flume可靠性还体现在数据可暂存上面,当目标不可访问后,数据会暂存在Channel中,等目标可访问之后,再 进行传输
    Source和Sink封装在一个事务的存储和检索中,即事件的放置或者提供由一个事务通过通道来分别提供。这保证 了事件集在流中可靠地进行端到端的传递。

    Sink开启事务 Sink从Channel中获取数据 Sink把数据传给另一个Flume Agent的Source中 Source开启事务 Source把数据传给Channel Source关闭事务 Sink关闭事务

    首先就是说它有一个事务性,这个事务性什么意思呢?就是我们刚才已经提过了,它主要是和channel类型有关,刚刚我们说了channel类型有两个一个是file一个是memory对吧?为了保证消息不丢失,为了可靠就是你可以选择file对不对?通过file channel的方式去传输,然后另外一个就是说当我这个消息在传输的过程中,当传输到了下一个节点上,那如果要是说接收的这个节点出现了一些异常,比如说一些网络异常,那由此就会就可以导致你的数据就需要重发的,然后另外在同一个节点内,如果是source写入的数据,把这个数据已经写入到这个channel里面去,那这个数据在,比如说它写这个数据它也是成批成批写的,那同时在这批之类,它整体的数据出现了一些异常的话,那这个时候就所有的数据一旦有一个数据出现了异常,那同一批的其他数据都不会写入到channel里面去,那已经接收到的部分这批数据就直接被抛弃,然后这个时候靠上一个节点重新再发送一些数据,重新再补充一些数据进来,那这里面就涉及到了事务性,那flume是使用了一个事务性的方式来保证了传输event或者传输整个事件在整个过程中的可靠性,就是说在你sink必须在你的event传入到channel之后或者是已经这个event传输到下一个channel里面或者是你这个消息已经到了外部的数据目的地之后,就相当于你这个数据已经可以认为是被下游已经完整的接收到了,就是你的数据已经在下游非常可靠的落地了,这个时候你的event才能从你的channel中进行移除掉的,所以这样的机制才能保证你的event无论是在一个agent里面还是多个agent里面之间的流转都是可以保证可靠的,并且由于这样的一个事务保证,你整个event就可以被成功的存入起来。
    然后这是一个整个消息在传输过程中比如说一个端到端传递的一个步骤,就从你的前面的sink怎么把你的数据传输到下游的另一个agent里面去这么一个过程。
    好了这个flume大概我们之前也说了,可以支持一个很强的扩展性是不是?(如下图所示)

    你可以把它想象成一个乐高玩具一样,然后进行一个有效的拼装,比如说这是一个其中一个组件,然后把这个组件进行一个前后的关联,你可以把这个组件之间并行多套,或者你可以把这个组件进行上下游进行一个关联,就是上游的sink要对接到下游的source上面,也可以多个sink同时消息汇入到下游的同一个source里面去,并且你可以做更复杂的一些搭建,这些都可以通过简单的配置就可以去完成。
    复杂流动的目的就是说这根据你的业务场景的一个复杂程度了,那你每一套agent可能下游就是面对着业务处理的流程是不一样,这个是完全是可以根据你的进行一些选择,比如如下图所示

    每一个webserver上都部署一个agent对不对?那你这有三个webserver就对着三个agent对不对?那你在这个实际的架构过程中你的日志服务器是有很多节点的,那你不可能每一个节点,如果是没有这个Consolldation的话,你每一个节点都去往这个HDFS直接去写的话,这个就不太合适,因为你这个日志服务器它仅仅是用来做日志收集做的,而你把它权限在放开到再去写HDFS,它相当于是那个角色定位有些混乱,另外一个就是说你这个数据源并发同时去写的话对HDFS操作也不是一个很好的设计,所以最好把这里面数据都汇总到同一台或者是数目比较少的那几种,只要是你那个压力能扛得住的一些Consolldation进行一个集中处理,然后Consolldation再去对接到后台的一些存储服务,这样会前面和后面部分相当于它们面对的角色是不一样的。
    还有一个就比较典型了(如下图所示)

    这个sink和这个source的一个关联。然后我再看一下这个(如下图所示)

    这个上图其实有点像我们之前讲的路由选择,你看这个左边的source可能通过不同的方式去对接的,然后把这个不同的消息通过一个channel然后通过多个sink,每一个sink都对街到后面不同的应该是Consolldation,然后每一个Consolldation进行各自的一个日志收集,那你这里面就跟你的业务相关了,可能是你这个Consolldation之间收集的数据是一致的也有可能是不一致的对不对?,然后后面对着不同的sink,这不同的sink就往后端存储的时候你可以写到HDFS上也可以写到hbase上,因为你这个数据是可以,比如说这两个channel就对接着不同的存储是不是?那你上面的这个channel是往HDFS上去写,下面这个channel是往本地文件去写,但是你可以再搞一个channel然后往Hbase上去写都可以很随意。

    搭建http://note.youdao.com/noteshare?id=7d903eb22b05f0b4943389bfc5c6d51f&sub=1993705EC6A1439DB03B29D68D278BFC
    2  留言 2019-02-10 12:33:52
  • 大数据Storm平台8、Zookeeper

    前文链接:https://write-bug.com/article/2119.html
    前面很多文章已经多多少少涉及到了zookeeper,后面的文章也会更加依赖zookeeper,所以这里正式介绍一下:
    Zookeeper原理前文说过,其角色是生态中的一个权威角色,贯穿与整个生态,那么它为什么可以有效的协调不同机器之间的工作呢?
    zookeeper是一个分布式锁服务、名字服务器、分布式同步、组服务,基于Paxos协议在集群内访问任何一台机器得到result都是一样的。(Google内部实现叫Chubby)
    我们在单机开发的时候涉及到锁的是多线程开发:内存锁,互斥锁,读写锁等等,在一个进程内部对公共资源进行竞争,而一个进程空间的内存地址是一致的,所以导致不同的线程对同一块进程空间都是有操作权限的,代码与代码段有公共区域读写的时候会造成混乱,需要有一个锁来控制顺序关系。

    在分布式系统中,就不能通过几行代码去写一个锁解决机器与机器之间的协调和大量数据了,机器之间是独立隔离的,为了防止分布式系统中多个进程之间互相干扰需要有个进程进行调度,这个技术核心就类似于分布式锁服务。
    核心问题:没有一个全局的主控,协调和控制中心
    我们需要一个松散耦合(对硬件依赖不强)的分布式系统中粗粒度锁(粗到节点顺序工作就可以)以及可靠性存储(低容量存储数据)的系统去解决这个问题。
    数据模型:与标准文件系统很相似,但不能像Linux一样 : cd ..
    只能通过一个绝对路径去访问某一个节点,这有点像HDFS操作命令的路径,并且在zookeeper这么一个节点里面,它可以存储一些数据,并且他自然而然就给你配一些属性,这个属性就关于你数据的长度,创建时间,修改时间等等,然后你这个节点具有文件属性又有路径的一个特点,即它可以存数据,又可以通过一个绝对路径能够访问到指定的节点。
    在Zookeeper里面没有文件和路径的说法的,其实每个文件和目录都是一个节点
    那说到节点的话,这节点就有一定的属性了,节点属性有四个:

    Persistent Nodes:永久有效的节点,只要不手动删除(Client显式的删除),系统不崩溃文件永久存在
    Ephemeral Nodes:临时节点,仅在创建该节点client保持连接期间有效(心跳机制),一点连接丢失,zookeeper会自动删除该节点,(机器挂掉的时候所连接创建的该目录被删除,即使机器恢复也只能再次创建才可以)
    Sequence Nodes:(名字服务器(名字不重复))顺序节点,client申请创建该节点时,zookeeper会自动在节点路径末尾添加递增序列号,这种类型是实现分布式锁,分布式queue等特殊功能的关键;不允许单独存在,需要和前面两种任何一种同时存在

    监控机制

    getChildren():zookeeper是有监控功能的,可以监控某台机器是否生效,比如可以应用在HDFS2.0的主备切换机制里,如果主挂了,就可以监控并启动备份节点了,这个2.0中介绍的很详细了,那么如何监控到主挂了呢,就是通过临时节点监控这个机器节点的,如果这个机器一旦出现异常,临时节点就消失了,那么感知到节点消失这个事件发生的就是消失节点的父节点,再由父节点主动去上报备用节点或者不同场景的上层服务器节点(比如流量分发器), 比如网民访问新浪首页,流量分发器同时分发50%流量给两台服务器,但是server1挂了,创建的节点消失了,那流量分发器如何检测到server1挂了呢?父节点会主动上报(数据里面可以写是连接的哪个连接IP地址,其实流量分发器可以把父节点下的所有节点遍历一遍,就可以知道映射和连接的那个节点IP就知道哪个服务器挂了,不给分发流量就行了),以上是一个getChildren()的监控
    getData():节点数据发生变化的监控
    exists():节点是否存在

    三个关键点:

    一次性监控,被触发后,需要重新设置(只上报一次,上报一次后节点挂了或者重新恢复节点时候,父节点已经不会通知了,每次需要触发的时候就要重新设置)
    保证先收到事件,再收到数据修改的信息
    传递性,如create或者delete会触发节点监控点,同时也会触发父节点的监控点

    风险:

    客户端看不到所有数据变化,比如说网络原因(比如由于网络IO接收不到监控变化)
    多个事件的监控,有可能只会触发一次。一个客户端设置了关于某个数据点exists和get Data的监控,则当该数据被删除的时候只会触发被删除的通知
    客户端网络中断的过程无法收到监控的窗口时间,要由模块进行容错设计

    数据访问权限
    zookeeper本身提供了ACL机制,表示为 scheme: id: permissions,第一个字段表示采用哪一种机制,第二个id表示用户,permissions表示相关权限(如只读,读写,管理等),每个节点上的“访问控制连”(ACL,Access Control List)保存了各客户端对于该节点的访问权限
    例:
    IP:19.22.0.0/16,READ 表示IP地址以19.22开头的主机有该数据节点的读权限
    权限:

    CREATE 有创建子节点的权限
    READ 有读取节点数据和子节点列表的权限
    WRITE 有修改节点数据的权限,无创建和删除子节点的权限
    DELETE 有删除子节点的权限
    ADMIN 有设置节点权限的权限

    模式机制:

    World 它下面只有一个id, 叫anyone, world: anyone代表任何人,zookeeper 中对所有人有权限的结点就是属于world: anyone的
    Auth 已经被认证的用户(可以用过用户名:密码的方式,kerberos)
    Digest 通过username:password字符串的MD5编码认证用户
    Host 匹配主机名后缀,如,host: corp.com匹配host: host1.corp.com, host: host2.corp.com,但不能匹配host: host1.store.com
    IP 通过IP识别用户,表达式格式为 addr/bits

    public class NewDigest {public static void main(String[] args) throws Exception {List<ACL> acls = new ArrayList<ACL>(); //添加第一个id,采用用户名密码形式 Id id1 = new Id("digest", DigestAuthenticationProvider.generateDigest("admin:admin"));ACL acl1 = new ACL(ZooDefs.Perms.ALL, id1);acls.add(acl1); //添加第二个id,所有用户可读权限Id id2 = new Id("world", "anyone");ACL acl2 = new ACL(ZooDefs.Perms.READ, id2);acls.add(acl2);// zk用admin认证,创建/test ZNode。 ZooKeeper zk = new ZooKeeper("host1:2181,host2:2181,host3:2181", 2000, null); zk.addAuthInfo("digest", "admin:admin".getBytes());zk.create("/test", "data".getBytes(), acls, CreateMode.PERSISTENT); }}
    API参考:http://zookeeper.apache.org/doc/r3.3.3/api/org/apache/zookeeper/ZooKeeper.html应用

    配置管理

    更新配置文件(过滤,地域等等策略),人工操作单机机器多时过麻烦,脚本配置机器压力过大,而在zookeeper集群中只需要sever都访问这个节点更新配置就好了,虽然是个一个节点,但背后是个集群,压力会被集群后的机器自然的分散开。

    集群管理

    监控机器状态-》比如临时节点getChildren()遍历选主-》临时节点+顺序节点:选择当前是最小编号的 Server 为 Master ,主节点挂掉时,临时节点消失,选新主节点自动加1,成为新主节点,当前的节点列表中又出现一个最小编号的节点,原主节点复活后想成为主节点,需要创建新节点尾号再加1成为一个普通节点。
    共享锁服务


    控制不同节点顺序(粗粒度)协同,与选主类似,先处理最小编号节点进程任务

    队列管理
    同步队列 • 所有成员都聚齐才可使用—getChildren方式父节点通知FIFO队列 • 生产消费者—最小编号先处理

    zookeeper安装
    参考:http://note.youdao.com/noteshare?id=168883c03a9eb0d7b2e0c5a0a0216e1a&sub=963C8CD231AD4BF5ACE97C2FCAD96431
    实践
    基本操作命令
    执行客户端 zkCli.sh

    ls / 查看当前目录
    create /text “test” 创建节点
    create -e /text “test” 创建临时节点
    reate -s /text “test” 创建序列节点
    get /test 查看节点

    Java代码操作
    1  留言 2019-02-09 14:24:57
  • 大数据分布式数据库7、HBase

    前文链接:https://write-bug.com/article/2108.html
    HBase原理上节我们介绍了Hive,其本质为MR Job,而HBase不同于Hive,它是一个面向存储的开源的非关系型分布式数据库(NoSQL),适合存储海量、稀疏数据(不保证每条记录每个字段都有值),大部分数据存在HDFS上,少量在自身内存cache中。
    Hbase(Nosql数据库)与关系型数据库对比:行存储:关系型数据库

    优点:写入一次性完成,保证数据完整性,写入时做检查
    缺点:数据读取过程中产生冗余数据,若有少量数据可以忽略

    列存储:Nosql数据库

    优点:读取过程,不会产生冗余数据,只查询了一个字段,特别适合对数据完整性要求不高的大数据领域
    缺点:写入效率差,保证完整性方面差

    优势:

    海量数据存储:大量数据存在HDFS中
    快速随机访问:在HDFS中更适合从头到尾的这种顺序读取,不适合随机访问,而Hbase只需要读取某一key的字段就可以了
    可以进行大量改写操作:为什么HDFS中不能呢?HBase在修改数据时,是在数据dump到磁盘之前还在cache中的时候做修改的,这样的速度是HDFS所不能比的,还有HBase在改写一个字段的时候,实质上还是在cache中完成操作的,而dump磁盘是一个异步的过程,其实修改后的数据与旧数据到了两个文件中,我们用户感知到的只是查询数据时候其返回了我们修改后的最新数据就可以了,这里我们下面会介绍的version号就知道了

    Hbase结构逻辑模型

    RowKey:有了这个key,我们才能查询到后面的value,是表中每条记录的“主键”,方便快速查找,Rowkey的设计非常重要。
    Column Family:列族,拥有一个名称(string),包含一个或者多个相关列
    Column qualifier:属于某一个columnfamily,familyName :columnName,每条记录可动态添加


    我们有了上面这三个信息,就可以定位到具体的列字段了。
    再看上面的表中有些cq中包含着几个timestamp时间戳,背后存着不同的value值,这就是前面说的改写具体字段存在不同的文件中。
    Version Number:类型为Long,默认值是系统时间戳,可由用户自定义,每一个时间戳的背后存着不同的版本,可以设置个数,默认显示最新时间戳的字段(即倒序)
    三维有序:
    三个维度:
    -rowkey
    -column family->column qualifier
    -version(timestamp)->value

    {rowkey => {family => {qualifier => {version => value}}}}
    a :cf1 :bar :1368394583:7

    有序:我们在存储数据时,系统会按照rowkey字典序去排序数据,然后里面列存储再按照cf、cq的字典序,和时间戳的倒序排序数据。
    物理模型我们前面介绍的数据模型格式都是以一张表格的形式去做剖析,为了我们所理解,而出现的逻辑概念。而真正机器存储时是怎么存储的呢?

    我们这里抽调出一张逻辑表格来看,假如这张表格数据量非常的大,一台机器很难存储一张表格,那我们计算机应该怎样去存储呢?肯定是把这张表格分片到很多台机器中才符合我们处理大数据的思想嘛,我们看到图中的数据依旧按照rowkey的字典序进行排序,并且上面切分为好多的region(区域),注意!这里的切分是按照行rowkey切分的。

    这里有一个细节:我们切分前后,里面的数据都是按照rowkey排好序的
    Hregion就相当于前面MR中的partition功能,进来一条数据,按照rowkey分配插入到一个region中,因为前面说了数据是按照key排好序的,所以这条数据就不可能分配到其他的region里面了,即相同的key也同样是肯定在同一个region上
    HBase集群分布数据的最小单位为region。想象成HDFS上的block,如果一台机器挂了,其上的所有block都将消失,数据也就失去了我们设置的3副本的平衡,需要去别的机器拷贝出数据到新的DN上来达到平衡和负载均衡(请求压力)。而HRegion是分布式存储负载均衡的最小单元,但不是最小存储的最小单元
    HRegion是物理概念(进程),Region是逻辑概念

    行锁定:假如我们rowkey为itemid,userA正在对一个itemid的字段做修改,与此同时,userB也想对这个字段的不同列做修改是不允许的,只有当时读写的进程具有绝对的主权,粒度按行锁定(粗)。

    前面说过,region数据分布就像HDFS中的block,所以换言之一个机器节点中有多个region,这个节点里面有一个进程叫Regionserver,主要负责用户的IO请求,与HDFS的交互,也就是一个HRegionserver进程中存储管理了多个HRegion对象(不一定来自于同一个table)。

    HRegion再划分,里面又分为多个Hstore,HStore是HBase中核心的存储单元。
    Hstore由两个部分组成:内存和HDFS文件
    即HStore=Memstore+StoreFile(HFile)
    HStore—-对应逻辑table中的columnFamily
    MapReduce中Memstore:100M->80%->往磁盘上spil
    Memstore: 写缓存,默认128M->将内存Flush为一个StoreFile,每个CF都有自己的Memstore
    HLog: WAL(Write-Ahead-Log)预写日志,避免数据丢失

    我们在往memStore和Hstore写数据时,假设正在写过程此Hregion挂了,那么我们写好的数据就会丢失,所以它不会直接写到某个机器上,而是先写到HDFS上,先有一个HDFS的公共区域(HLog),写完之后再写到一个Hregion里面去,所以这时Hregion挂了,我只需要从HLog中恢复数据就好了。
    每一个RegionServer都可以读取Hlog,管理维护多个Region,每一个Region都可以读HLog,如果Region不出现错误情况下,就和HLog没有任何交互,如果一个RegionServer挂掉,涉及到Region迁移,里面的数据已经消失了,是在Hlog中恢复到其他Region Server中,就像Mysql中的远程备份,replication,只是把Mysql操作的每一条sql语句重新执行一遍,就相当于恢复数据了。
    Client往Region Server 端提交数据时,会先写WAL日志,写成功后才会告诉客户端提交数据成功。在一个RegionServer上的所有的Region都共享一个HLog,一次数据的提交先写WAL,写入成功后,再写memstore,当memstore值到达一定阈值,就会形成一个个Store File(理解为Hfile格式的封装,本质为Hfile)
    blockcache:读缓存,加速读取效率(假设没有blockcache,读取顺序(指定好rowkey,CF: CQ):rowkey-》某HRegion-》CF-》Hstore-》MemStore-》二分法查找Store File(因为dump时rowkey是排序好的))读取后再缓存到blockcache中。

    这里引发一个问题:读取时可能会在memstore和多个Hfile中都出现相同的rowkey,这种情况怎么办呢,只能靠我们平时的合并机制(通常把很小的region merge到一起,手动完成,整合相同rowkey)了。

    分裂:我们在按照rowkey分配region时可能是同一个范围但一般每个region存储数据的规模是不一样的,随着数据插入,region随之增大,当增大到一个阈值时,按照大小平均分裂成两个新的region,分区参数默认10个G。
    Region数目:每一个region信息都要在zookeeper上有一个节点来维护。这之前还没讲过zookeeper,可以先类比HDFS的block在Name Node中作为元数据,不支持小文件。所以:

    太多:增加zookeeper负担,读写性能下降
    太少:降低读写并发性能,压力不够分散。

    对于region过大的要做切分,切分更小粒度的region分散到不同的regionserver上去缓解压力作负载均衡。
    这里出现一个问题:如果用户正在访问正在分裂的region上的数据时,是请求不到数据的,只能等到分裂完成后才能为用户继续服务。通常情况下,我们把自动切分禁止,请求量空闲时手动切分。
    Hbase架构
    我们在图中可以看到主要有这么几个角色(client、Hmaster、HRegionSever(本地化)、Zookeeper)

    HMaster(主):

    负载均衡,管理分配region
    DDL:增删改->table,cf,namespace
    类似于namenode上管理一些元数据,这里管理table元数据
    ACL权限控制
    可以启动多个Master,但真正服务只有一个master服务,Master挂了,其他的启动,进行一个选主。数据和storm一样都在zookeeper上,主挂了,不影响数据的读,但影响数据的写,还会影响region的分裂,也就是负载均衡,和storm的fail-fast很像,真正数据在hdfs上,zookeeper上存储的是元数据

    请查看HDFS元数据解析还有,https://blog.csdn.net/qq_33624952/article/details/79341477
    https://blog.csdn.net/vintage_1/article/details/38391209
    HRegionSever(从):—-通常和DN部署在一起,本地化原则。

    管理和存放本地的Hregion
    读写HDFS,提供IO操作,维护Table数据
    本地化,MapReduce中,数据不移动计算框架移动,是为了尽量减少数据迁移,在HBASE里面,Hregion的数据尽量和数据所属的DataNode在一块,但是这个本地化不能总是满足和实现,因为region是不断移动的,插入,不断分裂,一旦分裂就变了,什么时候本地化可以从不能保证到保证的过度呢,也就是合并。。。。

    Zookeeper:

    在master与zookeeper之间,master与regionsever之间提供心跳机制
    保证集群中只有一个Master
    存储所有Region的入口(ROOT)地址——新版本取消 ,比如操作一个表,第一件事要找到这张表所对应的服务器,第二找到存在那几个region上,读的key,定位到记录在哪个region,regionserver也就确定了,如果缓存里面没有这个表,就要去想办法找这个regionserver,第一步就要找到zookeeper上的相应地址,zookeeper为了寻址存储这个入口数据
    实时监控Region Server的上下线信息,并通知Master

    Client:

    寻址:通过zk获取HRegion的地址,同时client具有缓存rowkey-》HRegion映射关系的功能(读的数据表的机器地址预存,减少网络交互)加速RegionServer的访问
    表的设计rowkey设计原则:
    长度:最大长度64kb,越短越好,尽量不超过16byte(机器64位——》内存对齐8字节,所以控制在8字节整数倍可以获得最大性能)
    过长:内存利用率降低,不能缓存太多value数据
    分散:建议rowkey前加hash散列字段(程序位置固定生成)、反转—-避免固定开头解决热点问题
    唯一性
    反转例:
    IP:地区相同分在一个region中,负载不均匀:
    把IP地址倒序存储—思考:电话号码、时间戳
    hash散列字段例:
    加密算法:Hash(crc32,md5)
    CF设计原则:
    数量:在业务要求下,尽量少数量:建议一到两个。
    一个文件:传统行存储做法,如果想要访问个别列数据时,需要遍历每一列,效率低下。
    多个文件:列存储做法,过多时影响文件系统效率。
    折中方案:将不同CF数据列分开存储,比如把经常访问的类似的数据字段列尽可能分配到同一CF中。
    当某个CF数据flush时,其他CF也会被关联触发flush,如果CF设计比较多,一旦产生连锁反应,会导致系统产生很多IO,影响性能。
    flush和region合并时触发最小单位都是region,如果memstore数据少量时没有必要做flush
    Hbase容错在HMaster和HRegionSever连接到ZooKeeper后,通过创建Ephemeral临时节点,并使用Heartbeat机制维持这个节点的存活状态,如果某个Ephemeral节点失效,则Hmaster会收到通知,并作相应处理。
    HLog
    除了HDFS存储信息,HBase还在Zookeeper中存储信息,其中的znode信息:

    /hbase/root-region-server ,Root region的位置
    /hbase/table/-ROOT-,根元数据信息
    /hbase/table/.META.,元数据信息
    /hbase/master,当选的Mater
    /hbase/backup-masters,备选的Master
    /hbase/rs ,Region Server的信息
    /hbase/unassigned,未分配的Region
    Master容错:Zookeeper重新选择一个新的Master;无Master过程中,数据读取仍照常进行,region切分、负载均衡等无法进行
    Region Server容错:定时向Zookeeper汇报心跳,如果一旦时间内未出现心跳,Master将该RegionServer上的 Region重新分配到其他RegionServer上,失效服务器上“预写”日志由主服务器进行分割 并派送给新的RegionServer
    Zookeeper容错:一般配置3或5个Zookeeper实例

    Hbase操作安装笔记:http://note.youdao.com/noteshare?id=45a15ce17d5e6c0ead6db5f806d600d3&sub=3CB5F03947634E38811A45AE080F9028
    Hbaseshell操作
    pythonHbase
    mr+Hbase
    Java+Hbase
    Scala+Hbase
    Hive+Hbase
    2  留言 2019-02-08 14:31:02
  • 大数据分布式数据库6、Hive

    前文链接:https://write-bug.com/article/2091.html
    Hive原理
    一种Sql解析引擎—>把Sql语句解析成MR Job(本质就是MR)
    Hive中的表是纯逻辑表,就只是表的定义等,即表的元数据。本质就是Hadoop的目录/文件, 达到了元数据与数据存储分离的目的,本身不存储数据,真实数据存储在HDFS上,元数据存在MySql上
    读多写少,不支持数据改写和删除
    Hive中没有定义专门的数据格式,由用户指定,需要指定三个属性:

    列分隔符 ‘ ’ ‘\t’ ‘\001’
    行分隔符 \n
    读取文件数据的方法 TextFile、SquenceFile(二进制文件\<k,v>—-Java)、RCFile(面向列)


    为什么有了MR还用Hive?
    面向非开发人员,简单有效:
    word_countselect word, count(*) from ( select explode(split(sentence, ' ')) as word from article ) t group by wordHql和Sql区别

    可拓展性(用户自定义函数):

    UDF 普通函数 类比:map 一对一
    UDAF 聚合函数 类比:groupByKey 多对一
    UDTF 表生成函数 类比:flat Map 一对多

    数据检查:

    Hql:读时模式 读慢(读时检查、解析字段) load快(写时不需要加载数据)
    Sql:写时模式 读快 load慢(写时做索引、压缩、数据一致性、字段检查等等)

    Hive体系架构
    Cli用户接口, 进行交互式执行SQL: 直接与Driver进行交互

    JDBC驱动,作为JAVA的API:JDBC是 通过Thift Server来接入,然后发送给Driver

    Driver sql->MR

    元数据:是一个独立的关系型数 据库,通常Mysql,Hive会在其中保存表模式和其他系统元数据

    存储模式:
    derby(默认,本地单用户模式)—存储元数据结构信息meta store
    mysql 多用户模式

    本地
    远程 —-独立机器,互相解耦

    数据管理方式hive的表本质就是Hadoop的目录/文件 – hive默认表存放路径一般都是在你工作目录的hive目录里面,按表名做文件夹分开,如果你 有分区表的话,分区值是子文件夹,可以直接在其它的M/R job里直接应用这部分数据。

    Hive的create创建表的时候,选择的创建方式:

    create table 内部表
    create external table外部表(删表时只删除元数据)

    Partition

    分区表(/tablename/条件字段)表中的一个 Partition 对应于表下的一个目录,所有的 Partition 的 数据都存储在对应的目录中
    例如:pvs 表中包含 ds 和 city 两个 Partition,则

    对应于 ds = 20090801, ctry = US 的 HDFS 子目录为:/wh/pvs/ds=20090801/ctry=US;
    对应于 ds = 20090801, ctry = CA 的 HDFS 子目录为;/wh/pvs/ds=20090801/ctry=CA

    作用:辅助查询,缩小查询范围,加快数据的检索速度和对数据按照一定的 规格和条件进行管理。
    Bucket
    —分桶表(clusted by user into 32 buckets /lbs/mobile_user/action=insight/day=20131020/part-00000)数据采样sampling、控制数量
    —CLUSTERED BY —-bucket中的数据可以通过‘SORT BY’排序。
    ‘set hive.enforce.bucketing = true’ 可以自动控制上一轮reduce的数量从而适 配bucket的个数,
    当然,用户也可以自主设置mapred.reduce.tasks去适配 bucket个数
    —tablesample是抽样语句,语法:TABLESAMPLE(BUCKET x OUT OF y)
    查看sampling数据: – hive> select * from student tablesample(bucket 1 out of 2 on id); – tablesample是抽样语句,语法:TABLESAMPLE(BUCKET x OUT OF y) – y必须是table总bucket数的倍数或者因子。hive根据y的大小,决定抽样的比例。例如,table总共分了64份,当y=32 时,抽取(64/32=)2个bucket的数据,当y=128时,抽取(64/128=)1/2个bucket的数据。x表示从哪个bucket开始抽 取。例如,table总bucket数为32,tablesample(bucket 3 out of 16),表示总共抽取(32/16=)2个bucket的数据 ,分别为第3个bucket和第(3+16=)19个bucket的数据
    数据类型
    原生

    TINYINT
    SMALLINT
    INT
    BIGINT
    BOOLEAN
    FLOAT
    DOUBLE
    STRING
    BINARY(Hive 0.8.0以上才可用)
    TIMESTAMP(Hive 0.8.0以上才可用)

    复合

    Arrays:ARRAY<data_type>
    Maps:MAP<primitive_type, data_type>
    Structs:STRUCT<col_name: data_type[COMMENT col_comment],……>
    Union:UNIONTYPE<data_type, data_type,……>

    例:Map
    张三 “数学”:80,“语文”:90,“英语”:79
    李四 “数学”:70,“语文”:60,“英语”:79
    name scoreselect score from table{“数学”:80,“语文”:90,“英语”:79}{ “数学”:70,“语文”:60,“英语”:79}select name,score[‘数学’]from table
    张三 80
    李四 70
    例:Sql实现mr_join:
    INSERT OVERWRITE TABLE pv_usersSELECT pv.pageid,u.ageFROM page_view pvJOIN user uON(pv.userid=u.userid);
    例:Sql实现groupby —-mr_wordcountSELECT pageid,age,count(1)FROM pv_usersGROUP BY pageid,age
    Hive优化map优化优化并发个数减小
    set mapred.max.split.size=100000000;set mapred.min.split.size.per.node=100000000;set mapred.min.split.size.per.rack=100000000;set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;set mapred.map.tasks=10;—-适当加map数:几乎不起作用block大小会影响并发度set dfs.block.size(=128)hive.map.aggr=true ,相当于开启Combiner功能
    reduce优化优化并发个数
    set mapred.reduce.tasks=10
    mapreduce出现痛点:只有一个reduce的情况

    没有group by
    order by,建议用distribute by和sort by来替代

    order by:全局排序,因此只有一个reduce,当输入数据规模较大时,计算时间消耗严重sort by:不是全局排序,如果用sort by排序,并且设置多个reduce,每个reduce输出是有序的,但是不保证全局排序distribute by:控制map端的数据如果拆分给redcuce,可控制分区文件的个数cluster by:相当于distribute by和sort by的结合,但是只默认升序排序

    例子1:select * from TableA distribute by userid sort by itemid;
    例子2:select from TableA distribute by itemid sort by itemid asc;select from TableA cluster by itemid;

    笛卡尔积使用join的时候,尽量有效的使用on条件
    mapreduce出现痛点:如何加快查询速度(横向尽可能多并发,纵向尽可能少依赖)
    分区Partition
    Map Join:指定表是小表,内存处理,通常不超过1个G或者50w记录
    union all:先把两张表union all,然后再做join或者group by,可以减少mr的数量union 和 union all区别:union相当记录合并,union all不合并,性能后者更优
    multi-insert & multi group by查一次表完成多个任务
    Automatic merge:为了多个小文件合并
    – hive.merge.mapfiles = true 是否和并 Map 输出文件,默认为 True– hive.merge.mapredfiles = false 是否合并 Reduce 输出文件,默认为 False– hive.merge.size.per.task = 25610001000 合并文件的大小
    Multi-Count Distinct一个MR,拆成多个的目的是为了降低数据倾斜的压力必须设置参数:set hive.groupby.skewindata=true; 强制拆分,万能参数
    并行执行:set hive.exec.parallel=true 独立无依赖的MR

    mapreduce出现痛点:如何加快join操作
    语句优化:多表连接:如果join中多个表的join key是同一个,则join会转化为单个mr任务表的连接顺序:指定大、小表Hive默认把左表数据放到缓存中,右边的表的数据做流数据
    如果避免join过程中出现大量结果,尽可能在on中完成所有条件判断

    mapreduce解决数据倾斜问题
    操作 • Join • Group by • Count Distinct原因 • key分布不均导致的 • 人为的建表疏忽 • 业务数据特点症状 • 任务进度长时间维持在99%(或100%),查看任务监控页面,发现只有少量(1个或几个)reduce子任务未完成。• 查看未完成的子任务,可以看到本地读写数据量积累非常大,通常超过10GB可以认定为发生数据倾斜。倾斜度 • 平均记录数超过50w且最大记录数是超过平均记录数的4倍。 • 最长时长比平均时长超过4分钟,且最大时长超过平均时长的2倍。 万能方法 • hive.groupby.skewindata=true
    1、大小表关联
    Small_table join big_table
    2、大大表关联
    userid为0或null等情况,两个表做join

    方法一:业务层面干掉0或null的user
    方法二:sql层面key—tostring—randomon case when (x.uid = ‘-‘ or x.uid = ‘0‘ or x.uid is null) then concat(‘dp_hive_search’,rand()) else x.uid end = f.user_id;
    方法三:业务削减:先查询当天用户当小表做join

    3、聚合时存在大量特殊值
    select cast(count(distinct(user_id))+1 as bigint) as user_cntfrom tab_awhere user_id is not null and user_id <> ‘’
    4、空间换时间
    Select day,count(distinct session_id),count(distinct user_id) from log a group by day
    同一个reduce上进行distinct操作时压力很大
    select day, count(case when type='session' then 1 else null end) as session_cnt, count(case when type='user' then 1 else null end) as user_cnt from ( select day,session_id,type from ( select day,session_id,'session' as type from log union all select day user_id,'user' as type • from log ) group by day,session_id,type ) t1 group by day搭建参考:http://note.youdao.com/noteshare?id=22ac246cfddfe1e3f1a400a01a8f578a\&sub=34A84DA642E245E28E4C7B6172CD69FC
    实践:

    导入数据并统计
    join
    UDF
    从HDFS导入
    insert导入
    查询插入
    导出(local+hdfs)
    partition
    hive+hbase
    2  留言 2019-02-03 11:39:01
  • 大数据Spark平台5-1、spark-core

    前文链接:https://write-bug.com/article/2090.html
    下面开始新的章节:Spark
    MapReduce:分布式批量计算框架——对HDFS的强依赖。Spark-core:基于内存的分布式批量计算引擎/框架

    Spark的运行模式:

    单机模式:方便人工调试程序
    ./bin/run-example SparkPi 10 --master local[2] //一个进程模拟一个机器Standalone模式:自己独立一套集群(master/client/slave) 缺点:资源不利于充分利用
    ./bin/spark-submit --class org.apache.spark.examples.SparkPi --master spark://master:7077 lib/spark-examples-1.6.0-hadoop2.6.0.jar 100– Master/Slave结构• Master:类似Yarn中的RM• Worker:类似Yarn中的NM
    Yarn模式:

    Yarn-Client模式:Driver(spark Context)运行在本地Client。适合交互调试---本地和页面特定端口。



    Yarn-Cluster模式:Driver运行在集群(AM)。正式提交任务的模式(remote)--  yarn-cluster

    ./bin/spark-submit --class org.apache.spark.examples.SparkPi --master yarn-cluster lib/sparkexamples-1.6.0-hadoop2.6.0.jar 10Yarn Cluster vs. Yarn Client区别:本质是AM进程的区别,cluster模式下,driver运行在AM中,负责向Yarn申请资源 ,并监督作业运行状况,当用户提交完作用后,就关掉Client,作业会继续在yarn上运行。然而cluster模式不适合交互 类型的作业。而client模式,AM仅向yarn请求executor,client会和请求的container通信来调度任务,即client不能离开。
    Master/Slave结构

    RM:全局资源管理器,负责系统的资源管理和分配
    NM:每个节点上的资源和任务管理器
    AM:每个应用程序都有一个,负责任务调度和监视,并与RM调度器协商为任务获取资源


    Hadoop1.0中:

    一个MapReduce程序就是一个job,而一个job里面可以有一个或多个Task,Task又可以区分为Map Task和Reduce Task
    MapReduce中的每个Task分别在自己的进程中运行,当该Task运行完时,进程也就结束



    Spark中作业运行原理:

    Application:spark-submit提交的程序 ,一个Spark应用程序由一个Driver端和多个Executor组成,多个task 以多线程的形式运行在Executor中。
    Driver(AM):完成任务的调度以及executor和cluster manager进行协调

    Driver负责运行程序的main()方法,负责将代码发送到各个Executor去执行,然后取回结果,Driver端也是以Executor方式运行,可以运行在本地(Client),也可以运行在集群内部(Cluster),以Spark-shell形式运行的Spark应用程序,其Driver端都是运行在本地,以Spark-submit形式提交的作业可以通过参数- -deploy-mode来指定Driver端运行在哪里。 ———————-对应了集群中AM的功能
    构建SparkContext(Spark应用的入口,创建需要的变量,还包含集群的配置信息等)
    将用户提交的job转换为DAG图(类似数据处理的流程图)
    根据策略将DAG图划分为多个stage,根据分区从而生成一系列tasks
    根据tasks要求向RM申请资源
    提交任务并检测任务状态


    Executor(container): Executor是代码执行的地方,以进程的形式运行在节点之上(如standalone模式的Worker和yarn模式的NM)。每个worker node(NM)中有多个container(executor),是真正执行task的单元
    Task(线程): 一个Executor可以同时运行多个task(这里对应了上面说的一次提交多个任务,以多线程方式执行),每个task执行一个任务,窄依赖(split-》task一对一),其性质是以多线程的形式运行在Executor中。是Spark中最新的执行单元。RDD一般是带有partitions 的,每个partition在一个executor上的执行可以认为是一个 TaskCluster Manager(RM): 根据运行模式的不同(如Standalone,Apache Mesos,Hadoop YARN ,Kubernetes )其性质也会不同,主要是负责获取集群资源的外部服务(如standalone模式的Master和yarn模式的RM)。
    Job: 一个Action的触发即为一个job,一个job 存在多个task,和MR中Job不一样。MR中Job主要是Map或者Reduce Job。而Spark的Job其实很好区别,一个action算子就算一个 Job,比方说count,first等
    Stage:逻辑概念(dag) 分为水平关系(并行)和垂直关系(串),Job中如果有Shuffle(宽依赖,多对多)产生就会分为2个Stage,一个Job 可由多个Stage构成,一个Stage 可由多个task 组成,是spark中独有的。一般而言一个Job会切换成一定数量 的stage。各个stage之间按照顺序执行
    为什么一个application有多个job?比如分别跑用户特征和物品特征两个任务,这里可以用一个spark-submit同时提交两个任务--一个应用,捏合到一块并发执行
    在mr中,只能分别提交执行,串行。
    为什么一个Job有多个Stage?
    有宽依赖的,shuffle的时候是串行的stage (有partition和计算的中间结果),窄依赖的是一对一的是并行的stage。
    这里的并行和上面的两个job并行,像上面的特征任务,可以达到同样的效果,但是数据倾斜的时候就一般会变为两个job
    (万能方法:hive.groupby.skwindata=true)详情:Hive数据倾斜解决方法总结
    为什么一个Stage里有多个task?
    task ---线程
    Spark开发和MR开发
    MR:Map脚本

    Reduce脚本

    Spark:--Scala

    Scala基础参考:http://www.runoob.com/scala/scala-basic-syntax.html
    算子
    算子是RDD中定义的函数,可以对RDD中的数据进行转换和操作。
    算子分类:

    Value数据类型的Transformation算子,这种变换并不触发提交作业,针对处理的数据项是Value型的数据。
    Key-Value数据类型的Transfromation算子,这种变换并不触发提交作业,针对处理的数据项是Key-Value型的数据对。
    Action算子,这类算子会触发SparkContext提交Job作业。

    RDD有两种操作算子:

    Transformation(转换)--变换非为两类:窄依赖和宽依赖
    Ation(执行)

    transformation是得到一个新的RDD,方式很多,比如从数据源生成一个新的RDD,从RDD生成一个新的RDD,Transformation属于延迟计算,当一个RDD转换成另一个RDD时并没有立即进行转换,仅仅是记住了数据集的逻辑操作。action是得到一个值,或者一个结果(直接将RDDcache到内存中),触发Spark作业的运行,真正触发转换算子的计算。
    所有的transformation都是采用的懒策略,如果只是将transformation提交是不会执行计算的,计算只有在action被提交的时候才被触发。

    输入:在Spark程序运行中,数据从外部数据空间(如分布式存储:textFile读取HDFS等,parallelize方法输入Scala集合或数据)输入Spark,数据进入Spark运行时数据空间,转化为Spark中的数据块,通过BlockManager进行管理。
    运行:在Spark数据输入形成RDD后便可以通过变换算子,如filter等,对数据进行操作并将RDD转化为新的RDD,通过Action算子,触发Spark提交作业。 如果数据需要复用,可以通过Cache算子,将数据缓存到内存。
    输出:程序运行结束数据会输出Spark运行时空间,存储到分布式存储中(如saveAsTextFile输出到HDFS),或Scala数据或集合中(collect输出到Scala集合,count返回Scala int型数据)。

    Transformation和Actions操作概况
    Transformation具体内容

    map(func) :返回一个新的分布式数据集,由每个原元素经过func函数转换后组成。---一对一的处理
    filter(func) : 返回一个新的数据集,由经过func函数后返回值为true的原元素组成。把不符合条件的数据过滤掉---过滤器
    flatMap(func) : 类似于map,但是每一个输入元素,会被映射为0到多个输出元素(因此,func函数的返回值是一个Seq,而不是单一元素)。---一对多,比如句子变token
    sample(withReplacement, frac, seed) : 根据给定的随机种子seed,随机抽样出数量为frac的数据。---采样
    union(otherDataset) : 返回一个新的数据集,由原数据集和参数联合而成。---合并----unionall(不做去重)
    groupByKey([numTasks]) : 在一个由(K,V)对组成的数据集上调用,返回一个(K,Seq[V])对的数据集。注意:默认情况下,使用8个并行任务进行分组,你可以传入numTask可选参数,根据数据量设置不同数目的Task。---对某一字段key聚合 user-》(Sequence)item:score item:score item:score
    reduceByKey(func, [numTasks]) : 在一个(K,V)对的数据集上使用,返回一个(K,V)对的数据集,key相同的值,都被使用指定的reduce函数聚合到一起。和groupbykey类似,任务的个数是可以通过第二个可选参数来配置的。---对不同的主键,value做计算
    join(otherDataset, [numTasks]) : 在类型为(K,V)和(K,W)类型的数据集上调用,返回一个(K,(V,W))对,每个key中的所有元素都在一起的数据集。---两个表做聚合,根据key
    groupWith(otherDataset, [numTasks]) : 在类型为(K,V)和(K,W)类型的数据集上调用,返回一个数据集,组成元素为(K, Seq[V], Seq[W]) Tuples。
    cartesian(otherDataset) : 笛卡尔积。但在数据集T和U上调用时,返回一个(T,U)对的数据集,所有元素交互进行笛卡尔积。
    sort
    partitionBy---分桶

    Actions具体内容

    reduce(func) : 通过函数func聚集数据集中的所有元素。Func函数接受2个参数,返回一个值。这个函数必须是关联性的,确保可以被正确的并发执行。
    collect() : 在Driver的程序中,以数组的形式,返回数据集的所有元素。这通常会在使用filter或者其它操作后,返回一个足够小的数据子集再使用,直接将整个RDD集Collect返回,很可能会让Driver程序OOM。----明文输出
    count() : 返回数据集的元素个数。---数据统计
    take(n) : 返回一个数组,由数据集的前n个元素组成。注意,这个操作目前并非在多个节点上,并行执行,而是Driver程序所在机器,单机计算所有的元素内存压力会增大,需要谨慎使用。
    first() : 返回数据集的第一个元素(类似于take(1))。
    saveAsTextFile(path) : 将数据集的元素,以textfile的形式,保存到本地文件系统,hdfs或者任何其它hadoop支持的文件系统。Spark将会调用每个元素的toString方法,并将它转换为文件中的一行文本。
    saveAsSequenceFile(path) : 将数据集的元素,以sequencefile的格式,保存到指定的目录下,本地系统,hdfs或者任何其它hadoop支持的文件系统。RDD的元素必须由key-value对组成,并都实现了Hadoop的Writable接口,或隐式可以转换为Writable(Spark包括了基本类型的转换,例如Int,Double,String等等)。
    foreach(func) : 在数据集的每一个元素上,运行函数func。这通常用于更新一个累加器变量,或者和外部存储系统做交互。

    Spark会产生shuffle的算子---宽依赖

    去重(distinct)聚合(reduceByKey、groupBy、groupByKey、aggregateByKey、combineByKey)
    排序(sortBy、sortByKey)重分区(coalesce、repartition)集合或者表操作(intersection、subtract、subtractByKey、join、leftOuterJoin)

    Spark基于弹性分布式数据集(RDD)模型,具有良好的通用性、容错性与并行处理数据的能力
    RDD( Resilient Distributed Dataset ):弹性分布式数据集(相当于集合),它的本质是数据 集的描述(只读的、可分区的分布式数据集),而不是数据集本身 ----一块内存区域的格式化表达,描述数据前后依赖关系

    RDD使用户能够显式将计算结果保存在内存中,控制数据的划分,并使用更丰富的操作集合来处理
    使用更丰富的操作来处理,只读(由一个RDD变换得到另一个RDD,但是不能对本身的RDD修改)
    记录数据的变换而不是数据本身保证容错(lineage) ---DAG

    通常在不同机器上备份数据或者记录数据更新的方式完成容错,但这种对任务密集型任务代价很高
    RDD采用数据应用变换(map,filter,join),若部分数据丢失,RDD拥有足够的信息得知这部分数据是如何计算得到的,可通过重新计算来得到丢失 的数据
    这种恢复数据方法很快,无需大量数据复制操作,可以认为Spark是基于RDD模型的系统

    懒操作,延迟计算,action的时候才操作
    瞬时性,用时才产生,用完就释放
    Spark允许从以下四个方面构建RDD– 从共享文件系统中获取,如从HDFS中读数据构建RDD• val a = sc.textFile(“/xxx/yyy/file”)– 通过现有RDD转换得到• val b = a.map(x => (x, 1))– 定义一个scala数组• val c = sc.parallelize(1 to 10, 1)– 有一个已经存在的RDD通过持久化操作生成• val d = a.persist(), a. saveAsHadoopFile(“/xxx/yyy/zzz”)

    RDD的依赖关系

    窄依赖:不关心一条数据是从哪里来的,比如flatmap虽然是一到多,但是不关心纪录是哪里来的,只是把它分割为多条数据
    宽依赖:需要对key有一个严格的要求。有partition

    从后往前,将宽依赖 的边删掉,连通分量 及其在原图中所有依赖的RDD,构成一个 stage
    DAG是在计算过程 中 不 断 扩 展 , 在 action后才会启动计算
    每个stage内部尽可 能多地包含一组具有 窄依赖关系的转换, 并将它们流水线并行化(pipeline)


    RDD容错
    中间某一环节出现错误,部分数据失效(框架判断),这部分数据可以找到最近数据源(缓存)从新计算弥补回来
    一般如果要做cache的话,最好在shuffle类的算子后做cache,这类算子为宽依赖算子,数据很不容易得到,需要依赖于上游所有数据都准备数据之后才执行,哪怕有一块数据失效,都不能执行,为了保证下游失效不重新计算,在这里做一个cache。--既有内存又有磁盘

    优化
    每一个进程包含一个executor对象,每个executor对象包含一个线程池,每个线程执行一个tasks
    线程池好处:省去了频繁启停进程的开销
    Task并发度概念

    每一个节点可以启动一个或多个executor进程
    每个进程由多个core(相当于虚拟的cpu(通过配置配成但不能超过真实机核数))组成,每个core一次只能执行一个task
    调参:

    master yarn-cluster
    num-executors 100
    executor-memory 6G
    executor-cores 4
    driver-memory 1G

    内存划分

    20%:execution:执行内存---shuffle类算子会消耗内存,有的还需要cache缓存,如果内存满溢,把数据写到磁盘上(split)
    60%:Storage:存储cache、presist、broadcast数据(分发小数据集相当于-file---比如在本地完成一个mapside-join去代替处理大数据集的框架(小数据集没有必要使用,完全可以放在内存中实现))

    broadcast:分发小程序数据—mapsite-join

    适合做黑名单,白名单,字典。。。比如分发一个itemid-》itemname就可以在本地数据集里做一个id-》name 的转换
    20%:程序运行
    调参:

    conf spark.default.parallelism=1000每个stage默认的task数量
    conf spark.storage.memory Fraction=0.5默认0.6
    conf spark.shuffle.memory Fraction=0.3默认0.2

    剩下的百分比就是程序运行的内存

    spark1.6版本之前:每个类的内存是相互隔离的,导致了executor的内存利用率不高,需要人工调配参数百分比来优化内存
    spark1.6版本之后(包含):execution和storage内存是可以相互借用的(不够用时动态调整),减少了OOM的情况发生,但也需要参数值作为它的参考。---总内存空间够用的话就不用落地到磁盘了。

    开发调优

    原则一:避免创建重复的RDD
    原则二:尽可能复用同一个RDD
    原则三:对多次使用的RDD进行持久化处理
    原则四:避免使用shuffle类算子
    原则五:使用map-side预聚合的shuffle操作

    一定要使用shuffle的,无法用map类算子替代的,那么尽量使用map-site预聚合的算子
    思想类似MapReduce中的Combiner
    可能的情况下使用reduceByKey或aggregateByKey算子替代groupByKey算子,因为 reduceByKey或aggregateByKey算子会使用用户自定义的函数对每个节点本地相同的key进行 预聚合,而groupByKey算子不会预聚合

    原则六:使用Kryo优化序列化性能

    Kryo是一个序列化类库,来优化序列化和反序列化性能
    Spark默认使用Java序列化机制(ObjectOutputStream/ ObjectInputStream API)进行序列 化和反序列化
    Spark支持使用Kryo序列化库,性能比Java序列化库高很多,10倍左右


    Spark技术栈Spark Core : 基于RDD提供操作接口,利用 DAG进行统一的任务规划
    Spark SQL : Hive的表 + Spark的里。通过把 Hive的HQL转化为Spark DAG计算来实现
    Spark Streaming : Spark的流式计算框架
    MLIB : Spark的机器学习库,包含常用的机器 学习算法
    GraphX : Spark图并行操作库
    Spark搭建参考:http://note.youdao.com/noteshare?id=e8c9e725586227986f7abaeb7bada719&sub=74FBCEE204FE48F29C0A17370010B37C
    实践
    Scala-word countScala-UIgroupScala-cf—协同过滤pyspark-wordcountpyspark-jieba_word_clusterpyspark-LR+NB
    2  留言 2019-01-30 17:17:54
  • 大数据Hadoop平台4、Yarn

    前文链接:https://write-bug.com/article/2076.html
    在Haoop2.0中引入了Yarn这个分布式操作系统:
    在1.0中的 集群利用率过低,MR相当于灵魂的存在,想跑MR任务在一个HDFS上,想跑Spark.Storm又要创建个集群才行,不能共享资源,这时候我们想在一个集群中跑不同的计算框架任务这种需求,在1.0中就搞不定了
    1.0 组件相互独立,任务独立,整个的数据处理基本为MR包揽。
    2.0所有任务跑在一个集群,在这里,这些组件就相当于应用程序运行在其上,不同的计算框架任务都变成了一个很普通的插件了,这样就可以有效的利用集群资源了,yarn就相当于一个集群操作系统。

    作用:资源整合
    目的:系统资源利用最大化,在同一系统上运行不同的数据处理框架任务

    下面介绍一下Yarn的核心服务:

    ResouceManger—>RM资源管理器—->原来的Job Tracker
    Job—>application——-任务


    YARN负责管理集群上的各种资源,它也是master/slave结构:ResourceManager为master,NodeManager为slave,RM负责对NM上的资源进行统一管理和调度。当用户提交一个应用程序时(可以是MR程序、Spark程序或Storm程序等),会启动一个对应的ApplicationMaster用于跟踪和管理这个程序。它负责向RM申请资源,并在NM上启动应用程序的子任务。
    YARN的基本组成结构(组件)如下:
    1)ResourceManager——集群资源的仲裁者,1.0中Job Tracker的部分功能
    RM是一个全局的资源管理器,负责整个YARN集群上的资源管理和分配。它由如下两个组件构成:

    Scheduler(调度器):调度器根据各个应用的资源需求进行资源分配。资源分配单位用一个抽象概念“资源容器”(Resource Container,简称Container)表示。—-可插拔的调度组件(有很多配置选项)。—不负责应用程序的监控和状态跟踪,也不保证应用程序失败或者硬件失败的情况下对Task的重启

    FIFO Scheduler 按提交顺序,大任务阻塞
    Capacity Scheduler 专有队列运转小任务,但是小任务预先占一定集群资源
    Fair Scheduler 动态调整,公平共享

    Applications Manager(应用程序管理器):应用程序管理器负责管理整个系统中的所有应用程序,如启动应用程序对应的ApplicaitonMaster、监控AM运行状态并在失败时重启它。

    2)ApplicationMaster
    当客户端提交一个应用程序至YARN集群上时,启动一个对应的AM用于跟踪和管理这个程序。AM的主要功能包括:

    向RM调度器请求资源
    用户级别的作业进程,负责整个作业的运转周期
    监控所有任务的运行状态,并在任务运行失败时重新为任务申请资源以重启任务
    本质是一个特殊的container(Boss也是人嘛)——-随时关闭和启动,不需要一直启动

    注:AM是YARN对外提供的一个接口,不同的计算框架提供该接口的实现,如MRAppMaster、SparkAppMaster等等,使得该类型的应用程序可以运行在YARN集群上。
    3)NodeManager
    NM是每个节点上的资源和任务管理器。NM的主要功能包括:

    周期性向RM汇报(心跳)本(单)节点上的资源使用情况和Container的运行状态
    接收并处理来自AM的任务启动/停止等各种请求
    管理和启动container
    RM相当于山高皇帝远啊,通过NM(管理本地)就可以管辖整个集群

    4)Container——-资源—类似1.0的slot—-上面跑着真正的任务
    Container是YARN中的资源分配单位,它将内存、CPU、磁盘、网络等(任务运行环境)资源封装在一起。当AM向RM申请资源时,RM为AM返回的资源便是用Container的形式运转AM的应用。运行中的Container可能是一个Map,也可能是一个Reducer,Container也得和AM通信,报告任务的状态和健康信息。(任务级别)

    例子:RM是一个租房的中介老板,container是房源,如果来一个老板租房做美容院,那么不管他的分店还是办公地点都是不同的资源,办公楼管理着分店,那么办公楼就是个AM,管理着每个店面的销售额(任务调度),每一个地区都有一个销售经理,对接着所管辖区域的销售工作(但是NM同时一直监控管辖区域的房子完整和任务工作(自己店面资源)),这个销售经理就是个NM,然后美容店就可以开业运转了。

    1.0中JobTracker 是个独裁分子(资源管理、作业调度及监控),2.0Yarn中把其权力分权:RM、AM
    一个任务如果需要多个Container,则通过最小容量单位的多个Container来组成(为甚么yarn中存在虚拟Cpu的概念:根据具体硬件配置的不同,但也需根据实际情况)
    Yarn中优化:当MR执行结束,可以释放map的Container,请求更多的Reducer的Container
    不管是什么职位,大家工位都一样了,对于提交给集群的每个作业,会启动一个专用的、短暂的 JobTracker 来控制该作业中的任 务的执行,短暂的 JobTracker 由在从属节点上运行的 TaskTracker 启动不管AM(HR)在那个工位,他自己管理的团队和项目都能正常运转。
    Yarn优势

    减小了 JobTracker(也就是现在的 RM)的资源消耗,并且让监测每一个 Job 子任务 (tasks) 状态的程序分布式化了,更安全、更优美
    AM是一个可变更的部分,用户可以对不同的编程模型写自己的 AM,让更多类 型的编程模型(更多计算框架,不只是MR)能够跑在 Hadoop 集群中
    对于资源的表示以内存为单位,比之前以剩余 slot 数目更合理
    老的框架中,JobTracker 一个很大的负担就是监控 job 下的 tasks 的运行状况 ,现在,这个部分就扔给 ApplicationMaster 做了
    资源表示成内存量,那就没有了之前的 map slot/reduce slot 分开造成集群资 源闲置的尴尬情况(map释放后可以用于reduce)

    容错能力:RM挂掉:HA—-zookeeper
    NM挂掉:心跳告知RM,RM通知AM,AM进一步处理
    AM挂掉:RM负责重启,RM上有一个RMAM,是AM的AM,保存了已完成的信息,不再运行

    HDFS对Map Reduce彻底的重构—>MRv2或者Yarn
    MRv1: 不适合场景:实时(storm)、迭代(机器学习,模型训练)、图计算场景(Giraph)
    MRv2执行流程
    1、作业提交阶段
    作业提交阶段主要在客户端完成,过程如下图:

    当作业提交成功后,在HDFS上可以查看到相关作业文件:

    这些文件都是在Client端生成并上传至HDFS,对这些作业文件的说明如下:

    作业jar包:job.jar
    作业输入分片信息:job.split、job.splitmetainfo
    作业配置信息:job.xml

    这些作业文件存放在/tmp/hadoop-yarn/staging/hadoop/.staging/job_1435030889365_0001路径下,该路径又称为作业提交路径submitJobDir,它包括如下两部分:

    jobStagingArea:/tmp/hadoop-yarn/staging/hadoop/.staging是作业的staging目录
    jobID:唯一标识集群上的一个MR 任务

    2、作业的初始化阶段

    对上图更详细的表述如下:

    步骤1:当MR作业提交至YARN后,RM为该作业分配第一个Container,并与对应的NM通信,在Container中启动作业的MRAppMaster。
    步骤2:MRAppMaster向RM注册自己。这使得客户端可以直接通过RM查看应用程序的运行状态。
    步骤3:MRAppMaster读取作业的输入分片。作业的分片数(split)决定了启动的map任务数。

    3、作业执行阶段

    对上图更详细的表述如下:

    步骤1:MRAppMaster采用轮询的方式向RM申请任务所需资源。
    步骤2:资源分配成功后,MRAppMaster就与对应的NM通信,并启动YarnChild来执行具体的Map任务和Reduce任务。在任务运行前,还要将任务所需文件本地化(包括作业Jar包、配置信息等)。

    注:每个输入分片对应一个Map任务;Reduce任务数由mapreduce.job.reduces属性确定。

    步骤3:在YARN上运行时,任务每3秒钟向AM汇报进度和状态。
    步骤4:应用程序运行完成后,AM向RM注销自己,并释放所有资源。

    至此,MR作业执行结束。下图附上《Hadoop权威指南》中关于YARN上运行MR作业的流程图

    说明:上述步骤并没有考虑Shuffle过程,这是为了方便表述。
    1  留言 2019-01-30 17:03:55
  • 大数据Hadoop平台3-2、 HDFS2.0

    前文链接:https://write-bug.com/article/2058.html
    这节我们来介绍下HDFS2.0是如何一步步去解决1.0中存在的问题的,并在此基础上介绍2.0存储原理机制:
    首先我们先列出几个2.0的新特性,由这个为主线:

    NameNode HA 数据高可用
    NameNode Federation 联邦
    HDFS 快照
    HDFS 缓存
    HDFS ACL


    在Hadoop1.0整个集群只有一个NN,存在单点故障问题,一旦NN挂掉,整个集群也就无法使用了,而在1.0中的解决方法不管是NFS还是SNN都不是一个很好的解决办法,这个上节已经说了,所以HDFS2.0首当其冲的就是这个单点故障问题,引入了Name Node High Available(NNHA)高可用,不同于1.0中的不可靠实现(SNN),NNHA中有两个Name Node(分别为:ActiveName Node和StandByName Node),顾名思义,一个为工作状态一个为待命状态,当ANN所在服务器宕机时,SNN可以在数据不丢失的情况下,手工或自动切换并提供服务。
    ANN在集群中和原来的NN一样,承担着所有与客户端的交互操作,而SBNN在集群中不需要做任何交互操作,那他做什么呢?它只需要与ANN做好通信工作,什么意思,就是ANN数据发生变化时,SBNN和它做一个数据同步工作,这样就算我的ANN挂掉,SBNN可以随时顶上去成为新的ANN,提供快速的故障恢复。
    所以这里就引出一个问题:
    如何保持集群中两个NN的数据同步?HDFS2.0完整架构图:

    我们先来看下上面架构这个图,经过我们前面1.0的讲解,虽然这幅图确实发生了很大变化,但是我们先看下面一部分和前面还是很相似的,在1.0中我的NN和DN是通过心跳机制进行通信,DN去汇报block的健康情况、更新情况和任务进度,这里也不例外,只是DN同时向两个NN发送心跳罢了,目的就是保证两个NN维护的数据保持一致。而这里同步的数据是block->datanode的映射信息,每次心跳自动上报到NN中,所以NN中不必存储此类元数据。但是只靠心跳机制去维护还不够,因为还有内存中的path->blocklist去如何维护呢?两种方法:

    NFS(network file system):运维中的网络远程挂载目录,可以保证两台不同机器可以共享访问同一网络文件系统。(数据保存在了公共节点机器上)

    缺点:定制化硬件设备:必须是支持NAS的设备才能满足需求。复杂化部署过程:在部署好NameNode后,还必须额外配置NFS挂载、定制隔离脚本,部署易出错。简陋化NFS客户端:Bug多,部署配置易出错,导致HA不可用
    属于操作系统支持的配置——-fsimage镜像文件和edits编辑日志文件。

    QJM(基于Paxos(基于消息传递的一致性算法))最低法定人数管理机制:Hadoop自身提供的服务,借助了zookeeper,相当于把公共数据写在了zookeeper上,从而去维护两个NN上的log文件。

    属于应用软件级别的配置——用2N+1台机器存储editlog

    NameNode 为了数据同步,会通过一组称作Journal Nodes的独立进程进行相互通信。当active状态的Name Node命名空间有任何修改时,会告知大部分的JN进程,standby状态的Name Node再去读取JNs中的变更信息,并且一直监控editlog 中的变化,把变化应用到自己的命名空间,这样就可以确保集群出错时,命名空间状态已经是同步的了。
    Hadoop的元数据主要作用是维护HDFS文件系统中文件和目录相关信息。元数据的存储形式主要有3类:内存镜像、磁盘镜像(FSImage)、日志(EditLog)。在Namenode启动时,会加载磁盘镜像到内存中以进行元数据的管理,存储在NameNode内存;磁盘镜像是某一时刻HDFS的元数据信息的快照,包含所有相关Datanode节点文件块映射关系和命名空间(Namespace)信息,存储在NameNode本地文件系统;日志文件记录client发起的每一次操作信息,即保存所有对文件系统的修改操作,用于定期和磁盘镜像合并成最新镜像,保证NameNode元数据信息的完整,存储在NameNode本地和共享存储系统(QJM)中。
    如下所示为NameNode本地的EditLog和FSImage文件格式,EditLog文件有两种状态: inprocess和finalized, inprocess表示正在写的日志文件,文件名形式:editsinprocess[start-txid] finalized表示已经写完的日志文件,文件名形式:edits[start-txid][end-txid]; FSImage文件也有两种状态, finalized和checkpoint, finalized表示已经持久化磁盘的文件,文件名形式: fsimage[end-txid], checkpoint表示合并中的fsimage, 2.x版本checkpoint过程在Standby Namenode(SNN)上进行,SNN会定期将本地FSImage和从QJM上拉回的ANN的EditLog进行合并,合并完后再通过RPC传回ANN。
    name/├── current│ ├── VERSION│ ├── edits_0000000003619794209-0000000003619813881│ ├── edits_0000000003619813882-0000000003619831665│ ├── edits_0000000003619831666-0000000003619852153│ ├── edits_0000000003619852154-0000000003619871027│ ├── edits_0000000003619871028-0000000003619880765│ ├── edits_0000000003619880766-0000000003620060869│ ├── edits_inprogress_0000000003620060870│ ├── fsimage_0000000003618370058│ ├── fsimage_0000000003618370058.md5│ ├── fsimage_0000000003620060869│ ├── fsimage_0000000003620060869.md5│ └── seen_txid└── in_use.lock
    上面所示的还有一个很重要的文件就是seen_txid,保存的是一个事务ID,这个事务ID是EditLog最新的一个结束事务id,当NameNode重启时,会顺序遍历从edits_0000000000000000001到seen_txid所记录的txid所在的日志文件,进行元数据恢复,如果该文件丢失或记录的事务ID有问题,会造成数据块信息的丢失。
    HA其本质上就是要保证主备NN元数据是保持一致的,即保证fsimage和editlog在备NN上也是完整的。元数据的同步很大程度取决于EditLog的同步,而这步骤的关键就是共享文件系统,下面开始介绍一下关于QJM共享存储机制。
    QJM原理QJM全称是Quorum Journal Manager, 由JournalNode(JN)组成,一般是奇数点结点组成。每个JournalNode对外有一个简易的RPC接口,以供NameNode读写EditLog到JN本地磁盘。当写EditLog时,NameNode会同时向所有JournalNode并行写文件,只要有N/2+1结点写成功则认为此次写操作成功,遵循Paxos协议。其内部实现框架如下:

    从图中可看出,主要是涉及EditLog的不同管理对象和输出流对象,每种对象发挥着各自不同作用:
    FSEditLog:所有EditLog操作的入口JournalSet: 集成本地磁盘和JournalNode集群上EditLog的相关操作FileJournalManager: 实现本地磁盘上 EditLog 操作QuorumJournalManager: 实现JournalNode 集群EditLog操作AsyncLoggerSet: 实现JournalNode 集群 EditLog 的写操作集合AsyncLogger:发起RPC请求到JN,执行具体的日志同步功能JournalNodeRpcServer:运行在 JournalNode 节点进程中的 RPC 服务,接收 NameNode 端的 AsyncLogger 的 RPC 请求。JournalNodeHttpServer:运行在 JournalNode 节点进程中的 Http 服务,用于接收处于 Standby 状态的 NameNode 和其它 JournalNode 的同步 EditLog 文件流的请求。
    下面具体分析下QJM的读写过程。
    QJM 写过程分析
    上面提到EditLog,NameNode会把EditLog同时写到本地和JournalNode。写本地由配置中参数dfs.namenode.name.dir控制,写JN由参数dfs.namenode.shared.edits.dir控制,在写EditLog时会由两个不同的输出流来控制日志的写过程,分别为:EditLogFileOutputStream(本地输出流)和QuorumOutputStream(JN输出流)。写EditLog也不是直接写到磁盘中,为保证高吞吐,NameNode会分别为EditLogFileOutputStream和QuorumOutputStream定义两个同等大小的Buffer,大小大概是512KB,一个写Buffer(buffCurrent),一个同步Buffer(buffReady),这样可以一边写一边同步,所以EditLog是一个异步写过程,同时也是一个批量同步的过程,避免每写一笔就同步一次日志。
    这个是怎么实现边写边同步的呢,这中间其实是有一个缓冲区交换的过程,即bufferCurrent和buffReady在达到条件时会触发交换,如bufferCurrent在达到阈值同时bufferReady的数据又同步完时,bufferReady数据会清空,同时会将bufferCurrent指针指向bufferReady以满足继续写,另外会将bufferReady指针指向bufferCurrent以提供继续同步EditLog。上面过程用流程图就是表示如下:

    这里有一个问题,既然EditLog是异步写的,怎么保证缓存中的数据不丢呢,其实这里虽然是异步,但实际所有日志都需要通过logSync同步成功后才会给client返回成功码,假设某一时刻NameNode不可用了,其内存中的数据其实是未同步成功的,所以client会认为这部分数据未写成功。
    第二个问题是,EditLog怎么在多个JN上保持一致的呢。下面展开介绍。
    日志同步
    这个步骤上面有介绍到关于日志从ANN同步到JN的过程,具体如下:

    执行logSync过程,将ANN上的日志数据放到缓存队列中
    将缓存中数据同步到JN,JN有相应线程来处理logEdits请求
    JN收到数据后,先确认EpochNumber是否合法,再验证日志事务ID是否正常,将日志刷到磁盘,返回ANN成功码
    ANN收到JN成功请求后返回client写成功标识,若失败则抛出异常

    隔离双写:
    在ANN每次同步EditLog到JN时,先要保证不会有两个NN同时向JN同步日志。这个隔离是怎么做的。这里面涉及一个很重要的概念Epoch Numbers,很多分布式系统都会用到。Epoch有如下几个特性:
    当NN成为活动结点时,其会被赋予一个EpochNumber每个EpochNumber是惟一的,不会有相同的EpochNumber出现EpochNumber有严格顺序保证,每次NN切换后其EpochNumber都会自增1,后面生成的EpochNumber都会大于前面的EpochNumber
    QJM是怎么保证上面特性的呢,主要有以下几点:
    第一步,在对EditLog作任何修改前,QuorumJournalManager(NameNode上)必须被赋予一个EpochNumber第二步, QJM把自己的EpochNumber通过newEpoch(N)的方式发送给所有JN结点第三步, 当JN收到newEpoch请求后,会把QJM的EpochNumber保存到一个lastPromisedEpoch变量中并持久化到本地磁盘第四步, ANN同步日志到JN的任何RPC请求(如logEdits(),startLogSegment()等),都必须包含ANN的EpochNumber第五步,JN在收到RPC请求后,会将之与lastPromisedEpoch对比,如果请求的EpochNumber小于lastPromisedEpoch,将会拒绝同步请求,反之,会接受同步请求并将请求的EpochNumber保存在lastPromisedEpoch
    这样就能保证主备NN发生切换时,就算同时向JN同步日志,也能保证日志不会写乱,因为发生切换后,原ANN的EpochNumber肯定是小于新ANN的EpochNumber,所以原ANN向JN的发起的所有同步请求都会拒绝,实现隔离功能,防止了脑裂。
    NN切换时 恢复in-process日志
    为什么要这步呢,如果NN切换了,可能各个JN上的EditLog的长度都不一样,需要在开始写之前将不一致的部分恢复。恢复机制如下:

    ANN先向所有JN发送getJournalState请求;
    JN会向ANN返回一个Epoch(lastPromisedEpoch);
    ANN收到大多数JN的Epoch后,选择最大的一个并加1作为当前新的Epoch,然后向JN发送新的newEpoch请求,把新的Epoch下发给JN;
    JN收到新的Epoch后,和lastPromisedEpoch对比,若更大则更新到本地并返回给ANN自己本地一个最新EditLogSegment起始事务Id,若小则返回NN错误;
    ANN收到多数JN成功响应后认为Epoch生成成功,开始准备日志恢复;
    ANN会选择一个最大的EditLogSegment事务ID作为恢复依据,然后向JN发送prepareRecovery; RPC请求,对应Paxos协议2p阶段的Phase1a,若多数JN响应prepareRecovery成功,则可认为Phase1a阶段成功;
    ANN选择进行同步的数据源,向JN发送acceptRecovery RPC请求,并将数据源作为参数传给JN。
    JN收到acceptRecovery请求后,会从JournalNodeHttpServer下载EditLogSegment并替换到本地保存的EditLogSegment,对应Paxos协议2p阶段的Phase1b,完成后返回ANN请求成功状态。
    ANN收到多数JN的响应成功请求后,向JN发送finalizeLogSegment请求,表示数据恢复完成,这样之后所有JN上的日志就能保持一致。

    数据恢复后,ANN上会将本地处于in-process状态的日志更改为finalized状态的日志,形式如edits[start-txid][stop-txid]。
    通过上面一些步骤,日志能保证成功同步到JN,同时保证JN日志的一致性,进而备NN上同步日志时也能保证数据是完整和一致的。
    QJM读过程分析
    这个读过程是面向备NN(SNN)的,SNN定期检查JournalNode上EditLog的变化,然后将EditLog拉回本地。SNN上有一个线程StandbyCheckpointer(可以称为正在合并中的fsimage),会定期将SNN上FSImage和EditLog合并,并将合并完的FSImage文件传回主NN(ANN)上,就是所说的Checkpointing过程。下面我们来看下Checkpointing是怎么进行的。
    在2.x版本中,已经将原来的由SecondaryNameNode主导的Checkpointing替换成由SNN主导的Checkpointing。下面是一个CheckPoint的流向图:

    总的来说,就是在SNN上先检查前置条件,前置条件包括两个方面:距离上次Checkpointing的时间间隔和EditLog中事务条数限制。前置条件任何一个满足都会触发Checkpointing,然后SNN会将最新的NameSpace数据即SNN内存中当前状态的元数据保存到一个临时的fsimage文件( fsimage.ckpt)然后比对从JN上拉到的最新EditLog的事务ID,将fsimage.ckpt中没有,EditLog中有的所有元数据修改记录合并一起并重命名成新的fsimage文件,同时生成一个md5文件。将最新的fsimage再通过HTTP请求传回ANN。通过定期合并fsimage有什么好处呢,主要有以下几个方面:
    可以避免EditLog越来越大,合并成新fsimage后可以将老的EditLog删除可以避免主NN(ANN)压力过大,合并是在SNN上进行的可以保证fsimage保存的是一份最新的元数据,故障恢复时避免数据丢失
    主备切换机制要完成HA,除了元数据同步外,还得有一个完备的主备切换机制,Hadoop的主备选举依赖于ZooKeeper。下面是主备切换的状态图:

    从图中可以看出,整个切换过程是由ZKFC来控制的,具体又可分为HealthMonitor、ZKFailoverController和ActiveStandbyElector三个组件。
    ZKFailoverController: 是HealthMontior和ActiveStandbyElector的母体,执行具体的切换操作
    HealthMonitor: 监控NameNode健康状态,若状态异常会触发回调ZKFailoverController进行自动主备切换
    ActiveStandbyElector: 通知ZK执行主备选举,若ZK完成变更,会回调ZKFailoverController相应方法进行主备状态切换
    在故障切换期间,ZooKeeper主要是发挥什么作用呢,有以下几点:
    失败保护:集群中每一个NameNode都会在ZooKeeper维护一个持久的session,机器一旦挂掉,session就会过期,故障迁移就会触发
    Active NameNode选择:ZooKeeper有一个选择ActiveNN的机制,一旦现有的ANN宕机,其他NameNode可以向ZooKeeper申请排他成为下一个Active节点
    防脑裂: ZK本身是强一致和高可用的,可以用它来保证同一时刻只有一个活动节点
    那在哪些场景会触发自动切换呢,从HDFS-2185中归纳了以下几个场景:
    ActiveNN JVM奔溃:ANN上HealthMonitor状态上报会有连接超时异常,HealthMonitor会触发状态迁移至SERVICE_NOT_RESPONDING, 然后ANN上的ZKFC会退出选举,SNN上的ZKFC会获得Active Lock, 作相应隔离后成为Active结点。
    ActiveNN JVM冻结:这个是JVM没奔溃,但也无法响应,同奔溃一样,会触发自动切换。
    ActiveNN 机器宕机:此时ActiveStandbyElector会失去同ZK的心跳,会话超时,SNN上的ZKFC会通知ZK删除ANN的活动锁,作相应隔离后完成主备切换。
    ActiveNN 健康状态异常: 此时HealthMonitor会收到一个HealthCheckFailedException,并触发自动切换。
    Active ZKFC奔溃:虽然ZKFC是一个独立的进程,但因设计简单也容易出问题,一旦ZKFC进程挂掉,虽然此时NameNode是OK的,但系统也认为需要切换,此时SNN会发一个请求到ANN要求ANN放弃主结点位置,ANN收到请求后,会触发完成自动切换。
    ZooKeeper奔溃:如果ZK奔溃了,主备NN上的ZKFC都会感知断连,此时主备NN会进入一个NeutralMode模式,同时不改变主备NN的状态,继续发挥作用,只不过此时,如果ANN也故障了,那集群无法发挥Failover, 也就不可用了,所以对于此种场景,ZK一般是不允许挂掉到多台,至少要有N/2+1台保持服务才算是安全的。
    总结
    上面介绍了下关于HadoopHA机制,归纳起来主要是两块:元数据同步和主备选举。元数据同步依赖于QJM共享存储,主备选举依赖于ZKFC和Zookeeper。整个过程还是比较复杂的,如果能理解Paxos协议,那也能更好的理解这个。
    以上即是利用QJM和zookeeper实现HDFS的高可用,解决了单点故障问题。
    下面再介绍下另一特性:联邦Namenode Federation:—-解决了内存限制问题
    该特性允许一个HDFS集群中存在多个NN同时对外提供服务,这些NN分管一部分目录(水平切分),彼此之间相互隔离,但是本质还是共享了底层的DataNode资源,只不过上层分权了而已。

    架构设计:
    为了水平拓展Name Node,Federation使用了多个独立的Namenode/NameSpace。这些Name Node之间是联合的,也就是说他们之间相互独立且不需要相互协调,各自分工,管理自己的区域。分布式的datanode被用作通用的共享数据块存储设备,每个Datanode要向所有的NN注册,并周期性向所有NN发送心跳和块报告,并执行来自所有namenode的命令。
    这里引入了一个概念叫Block Pool块池,即单个命名空间的一组Block块(一个NN下维护多个DN),每一个命名空间可以根据大小和业务单独分配一个NN。
    DataNode是一个物理概念,Block Pool是一个重新将Block划分的逻辑概念,同一个dataNode中可以存在多个BlockPool的块,BlockPool允许一个命名空间NN在不通知其他NN的时候为一个新的Block创建Blockid,而一个NN失效同样也不会影响其下的DataNode为其他NN服务。
    每个BlockPool内部自治,不与其他Blockpool交流,一个NN挂了不影响其他NN。
    当DN和NN建立联系并开始会话后,自动建立BlockPool,每个Block都有一个唯一的表示,我们称之为拓展块ID,在HDFS集群中是唯一的,为集群归并创造了条件———blockid
    DN中的数据结构通过块池ID Block Pool id索引,即DN中的BlockMap,storage等都通过BPID索引。
    某个NN上的NameSpace和它对应的Block Pool一起被称为NameSpace Volume。它是管理的基本单位。当一个NN/NS被删除后,其所有DN上对应的Block Pool也会被删除。当集群升级时,每个NameSpace Volume作为一个基本单元进行升级。
    集群中提供多个NameNode,每个NameNode负责管理一部分DataNode:
    以上其实就可以理解为不同目录给不同部门管理,对不同命名空间做一个逻辑切分而已,其实下方存储还是没有变。
    如果一个目录比较大,建议用单独的NN维护。
    联邦本质:NN(元数据)与DN(真实数据)解耦,实际为共享数据
    HDFS快照快照snapshots是HDFS文件系统的只读的基于某时间点的拷贝,可以针对某个目录,或者整个文件系统做快照。快照比较常见的应用场景是数据备份,以防一些用户错误或灾难恢复。
    备份、灾备、快速恢复
    本质:也占空间,(但仅仅记录了block列表和大小而已,不涉及数据本身),即某个目录某一时刻的镜像,创建过程快O(1)
    快照的高效性实现:

    快照可以即时创建,耗时仅为O(1)。—excluding the inode lookup time
    只有当涉及到快照目录的修改被执行时,才会产生额外的内存消耗。而且内存消耗为O(M),其中M是被修改的文件或目录数。
    创建快照时,block块并不会被拷贝。快照文件中只记录了block列表和文件大小,不会做任何数据拷贝。
    快照不会对正常的HDFS操作有任何影响:创建快照以后发生的修改操作,被按操作时间的倒序(from newer to older)记录下来。所以当前的数据能被直接获取,而快照点的数据,则通过在当前的数据基础上减去执行过的操作来获取。

    • Snapshot 并不会影响HDFS 的正常操作:修改会按照时间的反序记录,这样可 以直接读取到最新的数据。• 快照数据是当前数据减去修改的部分计算出来的。• 快照会存储在snapshottable的目录下。• HDFS快照是对目录进行设定,是某个目录的某一个时刻的镜像• 对于一个snapshottable文件夹,“.snapshot” 被用于进入他的快照 /foo 是一个 snapshottable目录,/foo/bar是一个/foo下面的文件目录,/foo有一个快照s0,那么路径就是 :/foo/.snapshot/s0/bar
    • hdfs dfsadmin -allowSnapshot /user/spark • hdfs dfs -createSnapshot /user/spark s0 • hdfs dfs -renameSnapshot /user/spark s0 s_init • hdfs dfs -deleteSnapshot /user/spark s_init • hdfs dfsadmin -disallowSnapshot /user/spark注意:做过快照的目录,本身的父目录和子目录都不允许再次做快照。
    HDFS缓存
    1.0:user-》DN 经常读的数据提前加载到内存中
    2.0:集中式缓存—可指定要缓存的HDFS路径——-非递归,只缓存当前目录,缓存到当前机器本地内存中,DN Block-》DN内存

    HDFS中的集中缓存管理(Centralized cache management)是一种显式缓存机制,允许用户指定HDFS需要缓存的路径。NameNode将与磁盘上具有所需要的块的DataNodes进行通信,指示这些DataNodes将这些块缓存到 off-heap caches中。
    HDFS权限控制ACLHadoop中的ACL与Linux中的ACL机制基本相同,都是用于为文件系统提供更精细化的权限控制。首先参数上要开启基本权限和访问控制列表功能– dfs.permissions.enabled– dfs.namenode.acls.enabled
    基本命令操作:
    hdfs dfs -getfacl [-R] 获取目录和文件的ACL 信息– hadoop fs -getfacl /input/aclhdfs dfs -setfacl [-R] [-b |-k -m |-x ] |[--set ] 设置文件和目录的ACL信息– hdfs dfs -setfacl -m user:mapred:r-- /input/acl– hdfs dfs -setfacl -x user:mapred /input/aclhdfs dfs -ls 当ls的权限位输出以+结束时,那么该文件或目录正在启用一个ACL。请参考:Hdfs的ACL測试:https://www.cnblogs.com/mthoutai/p/6868846.html

    搭建这样的机制已在MR章节更新,这里再提醒下学习时可为体验:参考文章:https://www.cnblogs.com/selinux/p/4155814.html【192.168.87.150】master1:NN ZKFC RM【192.168.87.151】master2:NN ZKFC RM【192.168.87.155】slave1:DN NM ZK JN【192.168.87.156】slave2:DN NM ZK JN【192.168.87.157】slave3:DN NM【192.168.87.158】slave4:DN NM ZK JN
    这个章节没怎么写好,还在持续更新。
    2  留言 2019-01-26 19:11:09
  • 大数据Hadoop平台3-1、HDFS1.0

    前文链接:https://write-bug.com/article/2054.html
    前面的章节曾多次提到HDFS,而HDFS作为生态中一个不可或缺的角色足可说明其重要性。
    下面我们首先来介绍一下HDFS1.0:

    好我们进入no pic you say a j8 环节:那么这张图是什么意思呢?
    如图所示主要有三个角色:NameNode、DataNode、Client
    也就是主节点,从节点和客户端。简单地说,主节点主要存储着数据元信息,从节点存储着真实数据,client则是提交任务的客户端机器,其实本质就是向NN请求读写数据(在真正公司中是不会让你真正登陆到任何节点上的,只能是起另外一台机器去和master的地址通过配置文件起某种联系。),我们模拟是在master上提交任务的,后面会详细讲解这几个角色。
    HDFS 是一个主/从(Master/Slave)体系架构,由于分布式存储的性质,集群拥有两类节点 NameNode 和 DataNode。
    HDFS 由三个组件(本质是进程)构成:NameNode(NN)DataNode(DN)SecondaryNameNode(SNN)

    NameNode是主节点,也叫Master,一个hadoop集群只有一个NameNode
    DataNode是从节点,也叫Worker,一个hadoop集群可以有多个DataNode,一个节点只有一个Data Node
    SecondaryNameNode不是第二主节点,是助手节点,作用于数据持久化,你可以认为是一个镜像文件,备份。

    相信上面这几点细节,我们在前面搭建1.0集群时查看jps进程已经体会到了。
    client读取数据的时候,并不是从我们的namenode上读取数据,而是直接从datanode上读取,那他是怎么知道我要的数据在哪台datanode节点上呢?其实是NN告诉客户端的,我们后面会说这个问题。
    那么这个图下面的小方块是什么意思呢?
    HDFS数据的最小单位是block,一个Block默认大小是64M(可配置),这个可以直接影响到MapReduce并发个数,每个绿色相当于一个Block,如果有一个DataNode挂了,数据丢失,打破了3副本的平衡,由3变成2的时候,在配置文件里配3的目的是数据高可用性,所以要复制(Replication)一个副本,这样就保证了一个balance。
    client往NameNode上提交任务时,不是说client有请求时,我的NN和DN才进行交互,而是你来了一个任务,我就去查看我的DN有哪些是空闲的,再继续往空闲的上分配任务,那么NN是如何知道哪些DN是空闲的呢?是Heartbeat(心跳机制),这里我的NN通过这个机制管理了整个集群的DataNode,就相当于管理了所有DN上的Block。

    Heartbeat(心跳机制):NameNode与DataNode之间存在心跳连接,DataNode每隔3s就主动向NameNode汇报健康状况,资源利用情况、任务调度等。
    balance:当一个datanode挂了,block,为了达到平衡状态,内部数据会平均分配到其他datanode上。

    下面我们来详细介绍下这几个组件:
    NameNode(NN)1)管理着文件系统命名空间:维护着文件系统树及树中的所有文件和目录,存储元信息:

    文件名目录名及它们之间的层级关系
    文件目录的所有者及其权限
    每个文件块的名及文件有哪些块组成

    存储元数据信息(描述数据的数据)的映射关系path->blockid_list 这个是什么意思呢?我们在hadoop 命令查看文件的时候,我们直接可以看到目录,文件及里面的信息,对我们来说是透明的,但这个文件其实是被切分好后散落在不同的机器上的,而具体在哪里我们是不需要关心的,Name Node帮我们记录了文件包含了那些Block,而它仅仅告诉了我们存储在哪个block上的名字(block号),但对于client来说真正有意义的则是这些block在哪些机器上。
    block->datanode(节点地址) 在这个映射中我们就知道block是在哪台dataNode上了。
    这个时候我们再看上面那个图,刚才提出的问题client如何知道去哪台机器读取自己想要的信息:(这里解决了client访问时如何知道数据在哪个DataNode机器上的问题)
    那我们已经知道了去哪台机器上读取数据了,读取什么数据呢?这时候就涉及到了datanode的映射关系:block->path 这样我们就知道读取哪个具体路径信息了。所以datanode在Name Node里面只是作为value形式存在。
    那么元数据存储在什么地方呢:

    一:内存: 响应请求的数据在内存中—-真正对外服务的数据
    二:磁盘:数据持久化—-这里就引出了SNN的作用

    元数据放在内存中,重启机器或机器宕机时,内存中的数据消失,为防止数据丢失,定期把内存中的数据dump(转储)到edit logs中(引出延迟问题),这里需要用到SNN定期把edit logs中的数据merge到fsimage(元数据镜像文件—本地磁盘)中(引出二次延迟问题),这个过程被称为SNN的持久化过程,相当于备份。当机器重启时,load fsimage文件——-这里引出了数据备份更新时的延迟问题。
    2)通常搭建机器时,定位为master机器———-主
    Job Tracker也在其上运行,(但在大公司中有需要的情况下,这两个是可以部署在两台机器上的)——-本地化原则(就近原则)减少网络IO和拷贝时间成本。
    3)Hadoop更倾向存储大文件原因:
    一般来说,一条元信息记录会占用200byte内存空间。假设块大小为64MB,备份数量是3,那么一个1GB大小的文件将占用16*3=48个文件块。如果现在有1000个1MB大小的文件,则会占用1000*3=3000个文件块(多个文件不能放到一个块中)。我们可以发现,如果文件越小,存储同等大小文件所需要的元信息就越多,所以Hadoop更喜欢大文件。
    4)元信息持久化
    在NameNode中存放元信息的文件是 fsimage。在系统运行期间所有对元信息的操作都保存在内存中并被持久化到另一个文件edits中。并且edits文件和fsimage文件会被SecondaryNameNode周期性的合并。
    内存中的元数据定期存储到fsimage中:edits只dump修改的文件,fsimage是比较旧的文件,定期把相对实时的edits合并到fsimage中,在Secondary Name Node进程中执行。

    5)1.0时,还没有zookeeper和好的集群解决方案,所以一个集群中只有一个NN,但也因为如此,引起了单点故障问题,而以当时的技术,想要解决单点故障问题,只有NFS和SNN,SNN前面说了,负责镜像文件的备份,但还是有延迟问题存在,避免不了丢失部分数据,NFS是远程网络挂载硬盘,但是增加了时间成本,所以无论那种方法都不是一种好的解决方案。
    6)运行NameNode会占用大量内存和I/O资源,一般NameNode不会存储用户数据或执行MapReduce任务。
    NameNode只有一个进程,挂掉了就没了,而且数据变大了,不能持久化,内存不够,直接制约了集群的大小,这就是1.0的局限性。
    DataNode(DN)
    由block数据块组成,存储真实数据。根据NN指示做创建、复制、删除等操作。
    心跳机制:每3秒向NN发送心跳报告健康状况以及任务进程等。
    副本机制:防止数据丢失,保证数据冗余,数据高可用,是一个以空间换取安全性的机制。(同机架两个不同位置,第三个不同机架随机位置)
    通常搭建机器环境时,定位为Slave机器——-从

    TaskTracker 和其在同一机器上运行,和前面一样,本地化原则。
    Block—-数据块

    磁盘读写的基本单位
    HDFS默认数据块大小64MB,磁盘块一般为512B
    块增大可以减少寻址时间,降低寻址时间/文件传输时间,若寻址时间为10ms,磁盘传输速率为100MB/s,那么该比例仅为1%
    数据块过大也不好,因为一个MapReduce通常以一个块作为输入,块过大会导致整体任务数量过小,降低作业处理速度

    Block副本放置策略

    一:放在客户端相同的节点(如果客户端时集群外的一台机器,就随机算节点,但是系统会避免挑选太满和太忙的节点)
    二:放在不同机架(随机选择)的节点
    三:放在与第二个副本同机架不同节点上

    distance(/D1/R1/H1,/D1/R1/H1)=0 相同的datanode
    distance(/D1/R1/H1,/D1/R1/H3)=2 同一rack下的不同datanode
    distance(/D1/R1/H1,/D1/R2/H5)=4 同一IDC下的不同datanode
    distance(/D1/R1/H1,/D2/R3/H9)=6 不同IDC下的datanode


    5)如何保证数据完整性?
    在Client读写过程中为了保证数据完整性,HDFS中有一个数据校验:CRC32(循环冗余校验码)可以类比MD5,只是不同的加密方法,数据读写过程中,一共校验两次:client写到DN之前以每隔一个单位(512字节)创建一个校验码并加上数据(相当于打了个标记)写到DN上,第二次是DN接收数据时,同样每隔512求一个校验码和发送过来的比对,以上这样即保证了数据的完整性。
    第二方式:在后台存在一扫描进程:DataBlockScanner,一旦检测出问题Block,通过心跳通知NN,NN就会让DN修复此问题Block(即从副本上拷贝数据)
    Second Name node虽然在名字上看就像是第二节点,其实不是,他并不是NN的备份,只是一个SNN进程,前面我们已经引出了很多NN的容错机制,下面我们来介绍一下它的具体功能:
    这里主要有两个文件:

    fsimage:它是在NameNode启动时对整个文件系统的快照(镜像文件)
    edit logs:它是在NameNode启动后,对文件系统的改动序列(存在磁盘中)

    两个文件状态:数据是不断改动的,而NameNode是很少重启的,所以edit log是不断增大,而fsimage是比较旧的,永远也赶不上edit log文件的;
    NameNode重启或宕机那一瞬间,内存数据瞬间消失,如何才能找回数据呢?

    NameNode重启之后会先读fsimage,之前跟eidt,两者合并才能得到完整数据,这时edit log数据会比fsimage大很多,合并需要很长时间,这时就需要一个机制,尽可能想办法怎样减小你的eidt,并且让fsimage这个文件能够尽可能的保持最新的数据状态,这样的话NameNode重启的话就不需要有合并这样的影响了,它就可以从fsimage直接读数据然后启动。所以这时就引出了SecondNameNode。

    SecondNameNode作为助手节点,检查点节点,它的作用为:

    用来保存HDFS的元数据信息,比如命名空间信息、块信息等,由于这些信息是在内存的,SecondNameNode是为了考虑持久化到磁盘
    定时到NameNode去获取edit logs,并更新到fsimage[Secondary NameNode自己的fsimage]
    一旦它有了新的fsimage文件,它将其拷贝回NameNode中。(这时有两个数据一样的fsimage)
    NameNode在下次重启时会使用这个新的fsimage文件,从而减少重启的时间。

    Secondary NameNode所做的不过是在文件系统中设置一个检查点来帮助NameNode更好的工作。它不是要取代掉NameNode也不是NameNode的备份。
    NameNode把每一次改动都会存在edit log中,但是整个事件是由谁来触发的?(DataNode)
    元数据持久化的过程(SNN来完成):内存->edit log(磁盘)-> fsimage
    fsimage:多久加载一次?(重启才会加载)
    假如我们修改了数据,内存中的数据好改,但是磁盘中的镜像文件(磁盘中的镜像文件是由hadoop namenode-format生成的叫做fsimage)不好改,那么我们怎么做呢?客户端做的操作,内存中会立刻修改,同时会在磁盘中记录日志(这个日志也有名字:edits)。这样假如namenode宕机了,我们也可以根据日志记录恢复,如果数据过多,会让启动时间变长。所以namenode就会有一个助理secondary namenode,他会定期的将日志文件下载过来,而且第一次下载的时候会把镜像文件fsimage下载过来secondary namenode会有元数据计算引擎。它会把fsimage(镜像)加载到内存中变为内存元数据对象,然后元数据计算引擎会解析edits日志文件。一边加载,一边解读,一边修改元数据。等加载完之后这批数据就是比较新的了。接着secondary namenode要把元数据重新序列化成fsimage镜像文件,将其上传给namenode.这样做的话如果namenode宕机之后,它只要下载最新编号的镜像文件即可,而secondary namenode不必每次都下载镜像文件,因为它本身就拥有最新编号的镜像文件。它只需要下载最新编号的日志文件。但是是不是每次上传的日志以及镜像都会保存?当然不会,因为这样太占空间,对于镜像文件他会保留最近的两个,对于日志会多保留几个,但是过期的日志也不会马上删掉。SNN edit目录树

    NN edit目录树


    edits_inprogress文件为正在写的edit文件seen_txid文件:
    每重启一次这个数值就会再 + 1seen_txid文件记录的是edits滚动的序号,每次重启namenode时,namenode就知道要将哪些edits进行加载到内存.
    VERSION 文件 :
    各个属性的含义:
    namespaceID 是文件系统的唯一标识,在文件系统首次格式化之后生成;
    storageType 说明这个文件存储的是什么进程的数据结构信息(如果是 DataNode, 那么 storageType=DATA_NODE)
    cTime 表示NameNode 存储时间的创建时间,由于我的NameNode没有更新过,所以这里的记录值为0,以后对NameNode升级之后,cTime将会记录更新时间戳.
    layoutVersion 表示 HDFS 永久性数据结构的版本信息,只要数据结构变更,版本号也要递减,此时的HDFS也需要升级,否则磁盘仍旧是使用旧版本的数据结构,这会导致新版本的NameNode 无法使用;
    clusterID是系统生成或手动指定的集群ID,在-clusterid选项中可以使用它; (6) blockpoolID:是针对每一个Namespace所对应的blockpool的ID,上面的这个BP- 893790215-192.168.88.101-1383809616115就是在我的ns1的namespace下的存储块池的ID,这个ID包括了 其对应的NameNode节点的ip地址。

    备份过程:

    将hdfs更新记录写入一个新的文件——edits.new。将fsimage和editlog通过http协议发送至secondary namenode。将fsimage与editlog合并,生成一个新的文件——fsimage.ckpt。这步之所以要在secondary namenode中进行,是因为比较耗时,如果在namenode中进行,或导致整个系统卡顿。将生成的fsimage.ckpt通过http协议发送至namenode。重命名fsimage.ckpt为fsimage,edits.new为edits。等待下一次checkpoint触发SecondaryNameNode进行工作,一直这样循环操作。注:checkpoint触发的条件可以在core-site.xml文件中进行配置。fs.checkpoint.period表示多长时间记录一次hdfs的镜像。默认是1小时。fs.checkpoint.size表示一次记录多大的size,默认64M。模拟数据恢复hdfs文件系统删除之后的备份:删除之前hdfs文件系统上的所有的文件:

    接下来删除 dfs下的name文件:

    使用jps查看进程,发现hadoop进程还是启动着,原因是在内存中运行,可以杀死NameNode进程:

    然后手动启动namenode:hadoop-daemon.sh start namenode

    但是此时hadoop集群已经拒绝连接了,其他的机器也都是这样:

    并且在master上的namenode并没有启动成功:

    将namesecondary这个文件夹的内容全部复制到当前目录,取名叫作name

    复制好了这个namesecondary之后,再次启动namenode:

    这次能够正常启动,并且能够正常查询到hadoop集群上的hdfs文件系统上的文件,恢复了之前的样子.这两个工作目录 name 和 namesecondary 的结构是一样的.所以就可以用namesecondary来恢复.日过此时在hdfs上新建一个文件,然后把name目录删除,重复上面的恢复的步骤,那么这个新建的文件是不会恢复的,这个刚刚新建的文件还只是在日志文件里面,还没来得及做合并.namesecondary里面还没有这个元数据.所以恢复出来之后就会少一个文件.综上,namesecondary就可以做一个数据源的备份.checkpoint 的附滞作用:namenode 和 secondary namenode 的工作目录存储结构完全相同,所以,当namenode故障退出需要重新恢复时,可以从secondary namenode 的工作目录中将fsimage拷贝到namenode的工作目录,以恢复namenode的元数据.
    总结前面已经引出了部分1.0中存在的问题,总结下:

    单点故障NameSpace(命名空间的限制):由于Namenode再内存中存储所有的元数据(metadata),因此单个Namenode所能存储的对象(文件+块)数目收到Namenode所在JVM的heap(堆) size的限制。50G的heap能够存储20亿个对象,这20亿个对象支持4000个datanode,12PB的存储(假设文件爱呢平均大小为40MB)。随着数据的飞速增长,存储的需求也随之增长。单个datanode从4T增长到36T,集群的尺寸增长到8000个datanode。存储的需求从12PB增长到大于100PB。(内存的限制)
    性能的瓶颈:由于是单个Namenode的HDFS架构,因此整个HDFS文件系统的吞吐量受限于单个NameNode的吞吐量。
    隔离问题:由于HDFS仅有一个Namenode,无法隔离各个程序,因此HDFS上的一个实验程序很可能影响整个HDFS上运行的程序。
    集群的可用性:在只有一个Namenode的HDFS中,此Namenode的宕机无疑会导致整个集群的不可用。(低可用性)
    Namespace和Block Management的紧密耦合:Hadoop 1.x在Namenode中的Namespace和Block Management组合的紧密耦合关系会导致如果想要实现另外一套Namenode方案比较困难,而且也限制了其他想要直接使用块存储的应用。
    不适合小文件存储———数据块富余过多,浪费空间
    压缩文件——单独占一个split——-过大时存在问题
    内存问题—-元数据多时,内存不够存储
    不建议大量随机读(无法优化寻址时间成本),可以预读
    异步操作:速度快,不能保证数据完整性

    下面总结一下这里的可靠性保证:

    心跳机制
    副本机制
    CRC32数据校验
    DataBlockScanner
    SNN:保证元数据
    回收站:trash目录(core-site.xml设置)
    报告 命令 例:hdfs fsck /path -files - blocks -locations
    快照 命令

    HDFS 特性:

    能做什么

    存储并管理PB级数据处理非结构化数据 hive,hbase注重数据处理的吞吐量(延迟不敏感) 内存预写应用模式:write-once-read-many存取模式(无数据一致性问题)一次/user写入多次/user读取写错:通常是删除和追加不影响吞吐量
    不适合做

    存储小文件(不建议)大量随机读(不建议) 文件指针需要对文件修改(不支持)多用户写入(不支持)




    客户端调用 create()来创建文件,Distributed File System 用 RPC 调用 NameNode节点,在文件系统的命名空间中创建一个新的文件。NameNode 节点首先确定文件原来不存在,并且客户端有创建文件的权限,然后创建新文件。Distributed File System 返回 FSDOutputStream,客户端用于写数据。客户端开始写入数据,FSDOutputStream 将数据分成块,写入 Data Queue。Data Queue 由 DataStreamer 读取,并通知 NameNode 节点分配数据节点,用来存储数据块(每块默认复制 3块)。分配的数据节点放在一个 Pipeline 里。Data Streamer 将数据块写入 Pipeline 中的第一个数据节点。第一个数据节点将数据块发送给第二个数据节点。第二个数据节点将数据发送给第三个数据节点。DFSOutputStream 为发出去的数据块保存了 Ack Queue,等待 Pipeline 中的数据节点告知数据已经写入成功。写入一个datanode都说明写入数据成功,内部datanode会数据冗余。




    首先 Client 通过 File System 的 Open 函数打开文件,Distributed File System 用 RPC调用 NameNode 节点,得到文件的数据块信息。对于每一个数据块,NameNode 节点返回保存数据块的数据节点的地址。Distributed File System 返回 FSDataInputStream 给客户端,用来读取数据。客户端调用 stream的 read()函数开始读取数据。FSDInputStream连接保存此文件第一个数据块的最近的数据节点。DataNode 从数据节点读到客户端(client),当此数据块读取完毕时,FSDInputStream 关闭和此数据节点的连接,然后连接此文件下一个数据块的最近的数据节点。当客户端读取完毕数据的时候,调用FSDataInputStream 的 close 函数。在读取数据的过程中,如果客户端在与数据节点通信出现错误,则尝试连接包含此数据块的下一个数据节点。失败的数据节点将被记录,以后不再连接。
    同步与异步问题:

    同步:速度慢,但是可以保证全局数据一致性异步:速度快,但无法保证数据一致性—-HDFS用的就是异步操作
    本地模式

    本地模式:保证计算框架和任务调度管理部署在同一台机器上,体现本地化原则,尽量减少数据移动开销。类比后面的yarn功能把JobTracker的功能都分权了分为RM、AM。
    本地化原则针对map阶段,因为reduce阶段有个远程partition,不能保证是同一机器。

    下节我们介绍HDFS2.0原理,看看它是如何解决这些1.0中的故障问题的。这节还会持续修改
    4  留言 2019-01-23 20:15:29
显示 0 到 25 ,共 25 条
eject