集体注意力

每一秒钟,整个互联网上就有 60 万条信息在 Facebook 上分享,2 亿个 Email、10 万条 推文被发送,571 个新网站被建立,1.9E(1018)字节数据被交换[1]。海量的信息不断涌现, 但是全球的互联网用户总数却仅有 30 多亿(截止到 2014 年底)[1]。正如诺贝尔经济学奖 得主 Herbert Simon 早在 1971 年指出的,“......在一个信息丰富的世界中,拥有了信息就意 味着另一种稀缺,......,(这)就是信息接受者的注意力。......”[2]。故而,在信息过载时 代,注意力成为了首要稀缺资源。注意力不仅是人类行为的原始驱动力,更反映了人类的需求与愿望。因此,研究互联网上人类群体的注意力是如何分配、流动及耗散的就具有重大意义。

集体注意力

正是由于注意力的重要性与稀缺性,H. Simon[2]、M. Goldharbor 等人最早􏰀出并发展 了注意力经济的概念,开创了注意力定性研究的先河[3]。然而关于人类群体注意力的定量 化研究却直到 20 世纪 90 年代之后才真正展开 [4-6]。

集体注意力(Collective attention)一词是由 B. Huberman 最早􏰀出的[5],指大量群体 对有限信息资源的关注与访问,包括点击、评论、点赞、修改等[5]。早先的研究集中在注 意力在单个信息资源上的分配。例如,Wu 和 Huberman 利用新闻网站 Digg 的数据研究了新 闻的流行性、创新性对注意力的影响,同时发现了一种由信息排序策略所引发的相变行为 [6]。近年来,社交媒体的深入发展促使学者研究多个信息资源对有限注意力的竞争[7],这 种竞争可以被简化为一个临界分支过程并蕴含着临界现象[8]。另一方面,集体注意力在时 间上也呈现出爆发的特性,特别是在热点事件到来前后会具有不同的爆发模式[9][10],且这 种爆发行为与人脑神经元的放电模式具有一定的相似性[11]。集体注意力不仅展现出丰富的 时空动态,还对知识的创造[12,13]、股票涨落的预测[14]、政党选举结果的预测[15]、科学 家研究兴趣的转移[16]、极端事件的预测[17]等等都有着非常重要的影响。

参考文献

  • [1] Website of Internet live Stats, available from http://www.internetlivestats.com/
  • [2] Simon, H. A., Designing Organizations for an Information-Rich World, in Martin Greenberger, Computers, Communication, and the Public Interest, Baltimore, MD: The Johns Hopkins Press, 1971
  • [3]张雷:西方注意力经济学派研究,中国社会科学出版社,2009
  • [4] Huberman B A, Pirolli P L T, Pitkow J E, et al. Strong regularities in world wide web surfing. Science, 1998, 280(5360): 95-97.
  • [5] Wu F, Huberman B A. Novelty and collective attention. Proceedings of the National Academy of Sciences, 2007, 104(45): 17599-17601.
  • [6] Wu F, Huberman B A. Popularity, novelty and attention, Proceedings of the 9th ACM conference on Electronic commerce. ACM, 2008: 240-245.
  • [7] Weng L, Flammini A, Vespignani A, et al. Competition among memes in a world with limited attention. Scientific reports, 2012, 2.
  • [8] Gleeson J P, Ward J A, O’Sullivan K P, et al. Competition-induced criticality in a model of meme popularity. Physical review letters, 2014, 112(4): 048701.
  • [9] Lehmann J, Gonçalves B, Ramasco J J, et al. Dynamical classes of collective attention in twitter, Proceedings of the 21st international conference on World Wide Web. ACM, 2012: 251-260.
  • [10] Crane R, Sornette D. Robust dynamic classes revealed by measuring the response function of a social system. Proceedings of the National Academy of Sciences, 2008, 105(41): 15649-15653.
  • [11] Sanli C, Lambiotte R. Local variation of collective attention in hashtag spike trains. arXiv preprint arXiv:1504.01637, 2015.
  • [12] Ciampaglia G L, Flammini A, Menczer F. The production of information in the attention economy. Scientific reports, 2015, 5.
  • [13] Wu L, Baggio J, Janssen M A. The Dynamics of Collaborative Knowledge Production. arXiv preprint arXiv:1509.05083, 2015.
  • [14] Heiberger R H. Collective Attention and Stock Prices: Evidence from Google Trends Data on Standard and Poor's 100. PloS one, 2015, 10(8): e0135311.
  • [15] Eom Y H, Puliga M, Smailović J, et al. Twitter-based analysis of the dynamics of collective attention to political parties. PloS one, 2015, 10(7): e0131184.
  • [16] Huberman B A. Social Attention in the Age of the Web. Working together or apart: Promoting the next generation of digital scholarship, 2009, 62.
  • [17] Miotto J M, Altmann E G. Predictability of extreme events in social media. PloS one, 2014, 9(11): e111506.

集体注意力流动

然而,人类注意力不会仅集中在同一个数字资源上,还会在多个资源上流动、转移。 本项目的研究重点则是关注这种集体注意力之流,即大量用户在网络空间中的连续动作序 列(如点击、回复、点赞等)所形成的流动[18]。这种流动反映了人类集体的兴趣转移,因 此具有十分广泛的工业应用背景,包括点击流分析[19],Web 挖掘[20]、计算广告学[21]等。

注意力流网络

Jake等人创立了一种新的分析方法——注意力流网络,用于从整体的角度研究注 意力在信息资源上的流动,它可以综合考虑资源之间的超链接结构与人类集体行为。所谓 的注意力流网络就是一个加权有向图,其中节点是信息资源,有向连边代表用户跳转,权 重值代表了注意力的流量(即有多少用户从一个节点跳转到另一个)[22, 23]。同时,在整 个网络中存在两个特殊的节点:源和汇,它们代表注意力流量的入口(表示用户上线)和 出口(表示用户离开)。我们可以直接按照会话(Session,通常就是指一个固定的时间间隔, 如一小时)划分的用户行为数据来构建这样的注意力流网络,如图1所示[24]。

 

上图中,左边一栏为假想的多个用户分别在各自的一次会话中的浏览轨迹,A、B、C、...F 代表不同用户,1,2,3...5 代表数字资源。因此 A 用户在这一次会话中先后访问了 1、2、4、5 号数字资源。根据所有这些用 户的浏览轨迹可以构造注意力流网络如右图所示,其中连边上的数值表示有多少人次跳转。源代表用户的入口,有多少用户首先访问了资源 i,从源到 i 的流量就是多少。汇代表用户出口,从资源 i 有多少用户退 出,从 i 到汇的流量就是多少。这种构造网络的方法可以保证流量平衡性,即除了源和汇以外,网络所有节点的总入流都等于总出流。

如此构建的注意力流网络会自动处于流平衡状态(如图 1),这为我们􏰀供了分析的便利性[25]。另外,由于源和汇的引入,注意力流网络也可以分析用户的进入与流失模式,这 对于网站的流量分析来说非常重要[19]。目前,采用这套方法,我们已经研究了注意力流网 络的去中心化程度[23]、网络论坛的粘性[24]、互联网生态系统的几何化表示[18]等问题, 收到了良好的成效。同时,这种流网络作为一般性方法也被广泛应用到了生态系统、国际贸易等其它领域[26,27,22]。

在本项目中,我们主要关注两种不同尺度的注意力流网络,分别是域名级别的注意力流网络,以及网络社区的注意力流网络。如图2所示。

 

参考文献:

  • [18] Shi P, Huang X, Wang J, et al. A Geometric Representation of Collective Attention Flows. PloS one, 2015, 10(9): e0136243.
  • [19] A. Kaushik: Web Analytics 2.0 The Art of Online Accountability & Science of Customer Centricity, Sybex; 1 edition, 2009
  • [20] Liu B. Web data mining: exploring hyperlinks, contents, and usage data. Springer Science & Business Media, 2007.
  • [21] 刘鹏,王超:计算广告,人民邮电出版社,2015
  • [22] Guo L, Lou X, Shi P, et al. Flow distances on open flow networks. Physica A: Statistical Mechanics and its Applications, 2015, 437: 235-248.
  • [23] Wu L, Zhang J. The decentralized flow structure of clickstreams on the web. The European Physical Journal B, 2013, 86(6): 1-6.
  • [24] Wu L, Zhang J, Zhao M. The metabolism and growth of web forums. PloS one, 2014, 9(8): e102646.
  • [25] Zhang J, Lou X, Guo L. Universal patterns and constructal law in open flow networks. International Journal of Heat and Technology, Volume 34 (2016), Special Issue 1, pp.S75-S82
  • [26] Zhang J, Guo L. Scaling behaviors of weighted food webs as energy transportation networks. Journal of theoretical biology, 2010, 264(3): 760-770.
  • [27] Shi P, Zhang J, Yang B, et al. Hierarchicality of trade flow networks reveals complexity of products. PloS one, 2014, 9(6): e98247.

数据源

要研究集体注意力流,需要使用大量的用户行为数据。在这里,我们列出Jake研究组已经收集到的一系列数据集

印第安纳大学师生上网数据

数据存放

该数据存放在服务器中,需用户远程登入“210.31.72.215”连接服务器后,才能下载。 存放的路径为“E:/indiana/data”中,都是最终处理后得到的CSV文件。

若通过ftp方式访问,详情参考服务器公共数据

数据描述

本组数据是一组匿名的印第安纳大学上网点击流数据,记录了校园内用户从2006年10月到2008年2月的上网浏览行为。尽管本组数据属于一组简单的有偏数据,但包含的信息仍然十分丰富。其中,包含了123137个网站以及45563567的日均流量。尤其是针对top网站的研究(Facebook, Myspace,Yahoo),这将会是一个不错的数据集。 数据是通过对通过印第安纳边界路由器流量的镜像应用伯克利过滤包产生的。过滤包会自动匹配那些去往TCP80端口的流量,如果数据包含有请求,收集系统就会生成如下的一条记录:

  • 时间戳
  • 被请求的URL
  • 响应的URL
  • 一个布尔型的用户代理分类(浏览器或者机器人)
  • 一个布尔型的标记,该请求是否会产生内置或外置IU

有几个需要注意的地方:

  1. 对于客户端的并没有能够区别他们的信息,即没有任何MAC、IP地址或者其他唯一的索引来区分用户。
  2. 这里没有尝试对数据流的重新组装以及服务器的响应进行分析。

数据格式

原始数据集有两个收集集:

  1. raw:大约250亿的请求,只保留了响应的主机名。收集了从2006年9月26日到2008年3月3日的信息,其中损失的信息为98天,包含整个2007年6月。
  2. raw-url:大约286亿请求,保留了响应所有全部URL。收集了从2008年3月3日到2010年5月31日的信息,其中损失的信息为179天,包含整个2008年12月,2009年1月和2月。

整个数据集按小时划分为一个个文件。每个文件中的首行有一些列的标记,这些都可以被忽略点。每条记录的格式样例:

XXXXADreferrer

host

path

其中XXXX表示时间戳(32-bit unix值),A为每个用户代理标记(“B”为浏览器,其他为“?”),D为方向标记(“I”为外部的流进入IU接口,“O”代表内部的流到外部IU接口),referrer是访问来源的主机名或者URL(换行符终止),host为目标主机名(换行符终止),path为目标路径(换行符终止).

为了使用方便,我们对原始数据集进行了处理,分离出两类文件,一类文件按照每小时命名(例如:2008-02-10_00output.csv),记录网站跳转的情况。另一个记录了与之对应的具体网站名称(例如:2008-02-10_00webs.csv)。

具体的python代码如下:

import csv
import datetime
import sys
import numpy as np
import time as tm
import random
from scipy.sparse import *
from scipy import *
import scipy as sp
import matplotlib.pyplot as plt
import networkx as nx
from urlparse import urlparse
def file_len(fname):
    with open(fname,'rb') as f:
        for i, l in enumerate(f):
            pass
    return i + 1
def parturlclean(url):
    try:
        if url.replace('.','').isdigit(): return 'None'
        else:
            if len(url.split('.')) >=2 :
                if url[-6:]=='com.cn': return '.'.join(url.split('.')[-3:])
                return '.'.join(url.split('.')[-2:])
        return ''
    except:
        return 'None'

def ReadWholeHourNetwork(working_pathfile):
    filename=working_pathfile
    nums=0
    bfile=open(filename+"webs.csv", "rb")
    urls=[]
    with bfile as FileObj:
        for lines in FileObj:
            seg=lines.split('\r\n')
            urls.append(seg[0]) 
            nums+=1
    bfile.close()
    lasttime=0
   
    smatrix=np.zeros([nums+2,nums+2])
    addresses={}
    with open(filename+'output.csv','rb') as FileObj:
        i=0
        for lines in FileObj:
            segments=lines.split(',')
            #bigstring=bigstring+lines
            inn=long(segments[0])
            out=long(segments[1])
            flux=long(segments[2])
            addresses[inn]=1
            addresses[out]=1
            smatrix[inn,out]=flux
            #eliminate self-loop flows
            #if inn==out:
            #   smatrix[inn,out]=0
            i+=1
    #print smatrix[21,:]
    list1=range(nums)
    list2=[v for v in addresses.iterkeys()]
    diff=list(set(list1)-set(list2))
    #if len(diff)>0:
    #    print 'error:'+str(len(diff))
    #print list(addresses.itervalues())
    return(smatrix,urls)
def residuals(p,xx,yy):
    k,b=p
    return np.log(yy) - (k*np.log(xx)+b)
from scipy.optimize import leastsq
import os.path

#################################

head='2007-09-16_'
tail1='_00_00_+3600'
tail2='_00_01_+3599'
dotclick='.click'
file_path='H:/clickstream/click/2007-09-16.click/'
for hour in range(24):
    if hour<10:
        hours='0'+str(hour)
    else:
        hours=str(hour)
    fullname=head+hours+tail1
    f1=os.path.isfile(file_path+fullname+dotclick)
    if not f1:
        fullname=head+hours+tail2
        f1=os.path.isfile(file_path+fullname+dotclick)
        if not f1:
            print 'bad'
    file_name=fullname+dotclick
    filename=fullname
    filelen=file_len(file_path+file_name)
    clicks={}
    i=0
    addresses={}
    clicks={}
    with open(file_path+file_name,'rb') as FileObj:
        last=''
        start=-1
        end=-1
        eventtime=-1
        for lines in FileObj:        
            if (i-1)%3==0:
                bys=bytes(lines)
                #if len(bys)==1:
                if len(bys)<4:
                    i=i-1
                    last=last+bys
                else:
                    decs=[ord(bys[j]) for j in range(len(bys))]
                    if len(last)>0:
                        lst=[ord(last[j]) for j in range(len(last))]
                        decs=lst+decs
                        last=''
                        #decs.insert(0,[ord(last[j]) for j in range(len(last))])
                        #decs.insert(0,ord(last))
                    
                    bins=[bin(decs[j])[2:].zfill(8) for j in range(4)]
                    binary=''.join(bins[::-1])
                    value=int(binary,2)
                    eventtime=value
                    year=datetime.datetime.fromtimestamp(value).year
                    if year!=2007:
                        print 'error'
                        print abc[0]
                    if decs[4]==66 and decs[5]==79:
                        addr=[chr(decs[v]) for v in range(6,len(decs)-1)]
                        addr=''.join(addr)
                        addr=parturlclean(addr)
                        if len(addr)==0:
                            inde=0
                        else:
                            inde=addresses.get(addr,len(addresses)+1)
                            addresses[addr]=inde
                        start=inde
                    last=''
            elif (i-1)%3==1:
                addr=lines[:-1]
                teep=addr
                addr=parturlclean(addr)
                if len(addr)==0:
                    inde=-1
                else:
                    inde=addresses.get(addr,len(addresses)+1)
                    addresses[addr]=inde
                end=inde
                if start>=0 and end>=0 and eventtime>0:
                    clicks[len(clicks)]={1:start,2:end,3:eventtime}
                    start=-1
                    end=-1
                    eventtime=-1
            i+=1
            if i%100000==0:
                print float(i)/float(filelen)

file_path='G:/clickstream/click/2007-09-16.click/'
working_path='G:/clickstream/click/2007-09-16.click/indiana/data/'
allinflux=np.ndarray([0])
allflux=np.ndarray([0])
ks=np.ndarray([0])
bs=np.ndarray([0])
dissipationks=np.ndarray([0])
dissipationbs=np.ndarray([0])
dissipationtis={}
dissipationdis={}
head='2007-09-16_'
tail1='_00_00_+3600'
tail2='_00_01_+3599'
dotclick='.click'
influxes=np.zeros(24)
fluxes=np.zeros(24)
for hour in range(24):
    if hour<10:
        hours='0'+str(hour)
    else:
        hours=str(hour)
    fullname=head+hours+tail1
    f1=os.path.isfile(file_path+fullname+dotclick)
    if not f1:
        fullname=head+hours+tail2
        f1=os.path.isfile(file_path+fullname+dotclick)
        if not f1:
            print 'bad'
    file_name=fullname+dotclick
    filename=fullname
    filelen=file_len(file_path+file_name)
    print hour
    matrix,urls=ReadWholeHourNetwork(working_path+fullname)

    if np.size(np.nonzero(matrix))>0:
        influx=np.sum(matrix[0,:])
        flux=np.sum(matrix)
        dissipations=np.sum(matrix,0)-np.sum(matrix,1)
        dissipations=dissipations[1:-1]
        tis=np.sum(matrix,0)
        tis=tis[1:-1]
        bools=(tis>0)&(dissipations>0)
        tis=tis[bools]
        dissipations=dissipations[bools]
        if np.size(dissipations)>2:
            logdiss=np.log(dissipations)
            logbins=np.linspace(np.amin(logdiss),np.amax(logdiss),np.size(logdiss)/60)
            avgTi=np.ndarray([0])
            avgDi=np.ndarray([0])
            for yy in range(np.size(logbins)-1):
                startt=logbins[yy]
                enddd=logbins[yy+1]
                bools=(logdiss>=startt)&(logdiss<enddd)
                tisstemp=tis[bools]
                tivalue=np.mean(np.log(tisstemp))
                divalue=(startt+enddd)/2
                avgTi=np.r_[avgTi,np.exp(tivalue)]
                avgDi=np.r_[avgDi,np.exp(divalue)]
            bools=(avgTi>0)&(avgDi>0)
            avgTi=avgTi[bools]
            avgDi=avgDi[bools]
            rr = leastsq(residuals,[1,1],args=(avgTi,avgDi))
            k,b=rr[0]
            dissipationks=np.r_[dissipationks,k]
            dissipationbs=np.r_[dissipationbs,b]
            dissipationtis[hour]=tis
            dissipationdis[hour]=dissipations
                
    influxes[hour]=influx
    fluxes[hour]=flux
rr = leastsq(residuals,[1,1],args=(influxes,fluxes))
k,b=rr[0]
xplt=influxes
yplt=xplt**k*np.exp(b)
plt.loglog(influxes,fluxes,'o')
plt.hold(True)
plt.loglog(xplt,yplt,'-')
plt.hold(True)

运行后,得到的.csv文件的内容如下图所示:

 

其中,第一列为网站A的编号,第2列为网站B的编号,第三列为从A到B网站跳转的人数,具体的网址名可以从对应的‘webs’文件中查看。

处理后的读入代码

下面为读取处理过数据的matlab代码:

 

CNNIC中国用户数据

数据说明

这个数据是中国互联网信息中心用五年的时间,收集来自30000名志愿者的电脑操作数据集。每个志愿者在个人的电脑上装有一个数据获取程序。这个程序将每2秒扫描一下当前的激活窗口,如果发现激活的窗口改变,将在数据集中加入一条新的记录。为了使得分析更加的方便,我们这里随机选取了其中1000个志愿者1个月的上网数据,大约有1.2*10^8次条记录。

数据存放

所有的原始数据存放在服务器中,用户需登入后才能下载。存放地址为“E/CNNIC中国用户数据/url_200/”,用户可以用此构建网络。

若通过ftp方式访问,详情参考服务器公共数据


数据格式

数据以每个志愿者作为单独的文件记录,一共是1000个csv文件,每个文件中的内容如下:

 

数据读入

 

循环中的变量f就是整个文件后的读入结果。 其中f['T']为日期,f['URL']为站名

部分百度贴吧数据

数据描述

本数据集的数据来自百度贴吧活跃程度排在前1000的贴吧,并使用了这些贴吧一天的总共24个小时的数据量。这些数据没有用户ID在内的个人身份信息,数据里只有时间(第几个小时)以及用户在点击浏览时,从一个帖子跳转到另一个帖子的帖子编号。每次点击一个帖子的号码,用户的行为可能多种多样的,比如是在浏览帖子,回复评论别人发表的帖子,自己发布新的帖子,等等。

数据格式

下表显示的是有一个用户在第1个小时,从编号为2182799068的帖子开始点击浏览,在继续点击浏览了4个帖子后,停止浏览或者退出了贴吧,其中“s”代表用户浏览行为结束。

时间(第几个小时) 前一个帖子编号 后一个帖子编号
1 2182799068 2182804888
1 2182804888 2184359849
1 2184359849 2184586777
1 2184586777 2185975737
1 2185975737 s

读入代码

1.首先是处理原始百度贴吧文件名,防止乱码报错

import os

# 批量重命名
def rename_file(src_dir, base_name, sufix):
    src_files = os.listdir(src_dir)
    # print len(src_files)
    index = base_name
    for srcfile in src_files:
        new_name = str(index) + sufix
        old_file_path = os.path.join(src_dir, srcfile)
        new_file_path = os.path.join(src_dir, new_name)
        # print '正在将“%s”重命名为“%s”' % (old_file_path, new_file_path)
        if os.path.exists(new_file_path):
            continue
        os.rename(old_file_path, new_file_path)
        index += 1

# 原始贴吧数据文件夹,包含全部1000个贴吧各自24小时的数据
# 实际运行时,需要修改
src_dir_ = 'F:\\Data\\20130228\\'
sufix_ = '.txt'
base_name_ = 2016000
rename_file(src_dir_, base_name_, sufix_)

2.其次是处理贴吧数据并做保存(注意:需要自行安装networkx包

import os
import networkx as nx
from collections import defaultdict


def yield_file_path(main_path):
        dir_list = os.listdir(main_path)
        for _dir in dir_list:
            # 某一个贴吧24小时的数据所在目录
            parent_dir = os.path.join(self.main_path, _dir)
            data_file_list = os.listdir(parent_dir)
            for data_file in data_file_list:
                # 某一个贴吧某一小时的数据完整路径
                yield os.path.join(complete_parent_dir, data_file)


class FlowNetwork(object):

    def __init__(self, file_path):
        self.Di = defaultdict(int)          # 每个节点的Di
        self.Ti = defaultdict(int)          # 每个节点的Ti
        self.__source = 'o'                 # 源节点
        self.__sink = 's'                   # 汇节点
        self.__weight = 1                   # 默认权重
        self.__G = nx.MultiDiGraph()        # 图G
        self.G = nx.DiGraph()               # 合并重复边后的图G
        self.file_path = file_path          # 贴吧数据文件路径


    # 构建流网络并计算个节点的Di和Ti
    def create_flow_network(self):
        last_node = ''
        # 构建流网络(相同连边重复出现)
        with open(self.file_path, 'r') as f:
            for line in f:
                # 行格式:xxx xxx xx(时间 节点1 节点2)
                u, v = line.strip().split()[1:]
                self.__G.add_weighted_edges_from([(u, v, self.__weight)])
                if last_node != u:
                    # 如果当前的节点1,即u节点与last_node不同,则添加:源节点->u节点的连边
                    self.__G.add_weighted_edges_from([(self.__source, u, self.__weight)])
                last_node = v

        # 合并相同连边并计算Di和Ti
        for v, nbrs in self.__G.adjacency_iter():
            for nbr, edict in nbrs.items():
                # 节点v与邻居节点nbr所有连边的权重之和
                sum_weight = sum([d['weight'] for d in edict.values()])
                self.G.add_edge(v, nbr, weight=sum_weight)
                self.Ti[v] += sum_weight
                if nbr == 's':
                    self.Di[v] += sum_weight
        # 验证
        # print 'source节点出度%d\tsink节点入度%d' %
        # (sum(self.Di.values()), self.Ti[self.__source])

    # 保存流网络(格式:from,to,weight) 一个贴吧1个小时一份
    def save_flow_network(self, save_dir):
        # 当前需要保存的某小时贴吧文件名
        file_name = os.path.split(self.file_path)[1]

        father_dir = os.path.split(self.file_path)[0]
        father_dir_name = os.path.split(father_dir)[1]

        # 每一个贴吧24小时的流网络的保存目录
        save_dir = os.path.join(save_dir, father_dir_name)
        if not os.path.exists(save_dir):
            os.mkdir(save_dir)

        # 每一个贴吧某一小时的流网络的保存路径
        save_path = os.path.join(save_dir, file_name)
        if os.path.exists(save_path):
            os.remove(save_path)

        # print '正在处理文件:{0}'.format(file_name)
        with open(save_path, 'a') as fw:
            for v, nbrs in self.G.adjacency_iter():
                for nbr, edict in nbrs.items():
                    # 节点v与邻居节点nbr所有连边的权重之和
                    weight = edict['weight']
                    fw.write(v + ',' + nbr + ',' + str(weight) + '\n')

    # 保存UVt和PVt 一个贴吧24小时一份
    def save_uvt_pvt(self, save_dir):
        _UVt = self.Ti[self.__source]
        _PVt = sum([val for val in self.Ti.values()]) - _UVt
        # print '点击流网络的UVt:{0}'.format(_UVt)
        # print '点击流网络的PVt:{0}'.format(_PVt)

        father_dir = os.path.split(self.file_path)[0]
        father_dir_name = os.path.split(absolute_dir_path)[1]

        if not os.path.exists(save_dir):
            os.mkdir(save_dir)

        save_path = os.path.join(save_dir, father_dir_name+'.txt')
        if os.path.exists(save_path):
            os.remove(save_path)

        # print '正在处理贴吧:{0}'.format(father_dir_name)
        with open(save_path, 'a') as fw:
            fw.write(str(_UVt)+' '+str(_PVt)+'\n')

    # 保存Ti和Di (格式:ti di)一个贴吧24小时一份
    def save_ti_di(self, save_dir):

        father_dir = os.path.split(self.file_path)[0]
        father_dir_name = os.path.split(father_dir)[1]

        if not os.path.exists(save_dir):
            os.mkdir(save_dir)

        save_path = os.path.join(save_dir, father_dir_name+'.txt')
        if os.path.exists(save_path):
            os.remove(save_path)

        # print '正在处理贴吧:{0}'.format(father_dir_name)
        with open(save_path, 'a') as fres:
            for v in self.__G.nodes():
                if v == self.__source:
                    continue
                _ti = self.Ti[v]
                _di = self.Di[v]
                fres.write(str(_ti)+' '+str(_di)+'\n')

    # 保存node数和edge数(格式:node edge)一个贴吧24小时一份
    def save_node_edge(self, save_dir):

        father_dir = os.path.split(self.file_path)[0]
        father_dir_name = os.path.split(father_dir)[1]

        if not os.path.exists(save_dir):
            os.mkdir(save_dir)

        save_path = os.path.join(save_dir, father_dir_name+'.txt')
        if os.path.exists(save_path):
            os.remove(save_path)

        # print '正在处理贴吧:{0}'.format(father_dir_name)
        with open(save_path, 'a') as fres:
            _nodes = self.G.number_of_nodes();
            _edges = self.G.number_of_edges()
            fres.write(str(_nodes) + ' ' + str(_edges) + '\n')

if __name__ == '__main__':
    # all_data_directory为全部贴吧数据所在目录,实际运行时需要修改
    for fpath in yield_file_path(all_data_directory):
        fn = FlowNetwork(fpath)

        # print '正在从文件:{0}创建流网络'.format(fpath)
        fn.create_flow_network()

        # print '正在保存文件{0}的流网络'.format(fpath)
        fn.save_flow_network(save_dir1)

        # print '正在保存文件{0}流网络的UVt和PVt'.format(fpath)
        fn.save_uvt_pvt(save_dir2)

        # print '正在保存文件{0}流网络的Ti和Di'.format(fpath)
        fn.save_ti_di(save_dir3)

        # print '正在保存文件{0}流网络的节点数和边数'.format(fpath)
        fn.save_node_edge(save_dir4)
        # print '************************************************'

研究方法

我们主要采取开放流网络的研究方法来研究注意力流网络。相关内容,请参看流网络

主要结论

采用注意力流网络工具研究集体注意力流的好处就在于,这种网络可以揭示互联网系统的整体信息,特别是网站和网站之间的联系。为了更加一目了然地揭示互联网生态系统的整体信息,我们将网络中的所有节点都嵌入到一个高维空间中。具体方法可以参考流网络的嵌入。嵌入之后,每一个网站就获得了一个独一无二的空间位置。这种位置信息可以反映很多现象。

互联网生态系统

下面,我们以印第安纳大学的注意力流网络为例来展示网络的空间嵌入。

国际互联网生态系统

 

上图所示就是将网络中的所有节点都嵌入到一个20维空间中,然后再将其降维到二维空间之后得到的图。图中每一个节点就是一个网站。节点的大小表示的是访问该网站的注意力流量大小再取对数。节点的颜色是根据专业分类网站(http://sitereview.bluecoat.com/sitereview.jsp)所做出来的分类结果。例如,黄色节点表示搜索引擎/门户类网站;红色的节点表示教育类网站;绿色节点则表示娱乐类网站,……。

根据这张图,我们能够观察到如下几类现象:

  • 相同颜色的网站倾向于聚集在一起。例如,成人类网站明显地聚集到了整张图的左侧,而绿色的新闻/娱乐类网站则聚到了图的右下侧。
  • 少数几个大型网站聚到了中心位置,尤其是Google基本位于整张图的正中心。按照我们的算法,对于整个互联网生态越重要的网站就会越靠近中心。我们用圆圈的大小来表示该网站的访问流量。我们看到,虽然Google的流量相对于Myspace、Facebook来说并不是很大,但是它的位置却比MySpace和Facebook更加靠近中心,这彰显出Google对于整个互联网的中心作用。
  • 最后,所有的网站的分布基本形成了一个以Google为中心的球形。

 

我们可以按照到所有其他节点的平均距离来进行从小到大的排序,并与按照流量的方式进行比较,如上表所示。我们发现,流距离排序能够更好地反映出来每一个网站在整个生态系统中的地位和作用。而流量仅仅反映了一个网站点击量的多少,它并不能反映出网站之间的相互关系。所以,MySpace、MSN等网站流量可能很大(因为它们都是社交网站,大部分流量都是发生在内部的),但是它们在整个网络中的中心性却没有Google这样的搜索引擎重要。

整个互联网、注意力、注意力的耗散基本也呈现出球对称的分布形态。于是我们定量化地计算出不同圈层各个量的分布情况,概括为如下图形。

 

上图画出的是三种量(网站数、注意力流量、耗散流量)随着从里到外的半径方向进行累积所展现的S型曲线。其中横坐标R为空间中任意一点到Google的距离(我们发现Google非常靠近整个网络的重心位置,所以选择它为区域的中心);纵坐标对应的是相应变量的累积量。这里累积量T(R)的计算是这样的:以Google为圆心,以R为半径画一个d维球,然后计算这个球内包含的所有点的相应流量,其它累积量的计算也类似。

我们可以按照球的半径大小从里到外把这些网站分成三个不同的层次(两条虚线圆形成了分界线)。我们发现最里面的球包含了仅仅1/5的网站数量,但是流量却涵盖了整个生态系统的45%;第二圈则包含了全部网站的40%,而流量却仅仅只有25%;第三圈则是剩下的小网站。

中国互联网生态系统

与此类似,我们还可以用同样的方法分析中国互联网。下面就是利用CNNIC数据作出的类似结果。

 

图中右下角的D是利用CNNIC数据揭示出来的整个中国互联网的地图。其中,不同的颜色表示不同的聚类。这里面的聚类就是在嵌入空间后运用K-means聚类算法计算出来的结果。A图则放大展示了D图中心一部分区域的互联网。其它图则分别展示了不同类别的互联网在整个图形中的位置。

我们发现:

  1. 整个图形相比较上面的国际互联网地图具有明显的不对称性;尽管在核心区域,网站分布还基本是对称的;
  2. 不同类别的网站聚成了不同的团簇,例如:F图中高亮的蓝色节点就是Q&A类型的网站;
  3. 知名度高的、中心性的网站聚到了中心,例如百度、qq等。

同样的,我们根据流距离对整个网站的流行度进行了排序,选取了top17,点击量和PageRank结果进行对比:

 

根据排名,也同样出现了某些流量较大的网站在流距离排名并不靠前。

此外,我们也分析了基于中国互联网用户数据的流量、耗散和网站数分布。下图为他们的累积分布:

 

同样,他们的累积分布都呈现“S”型特性。我们也按照注意力流的分位数将整个网络生态分成3个层次。其中,最中心的区域只有1%的网站数,但却集中了40%的人类注意力。

和史的印第安纳大学的研究结果相比较,我们可以发现整个互联网的可视化形状存在差别,基于美国用户的可视化表明整个网络类似于一个对称的球形,所有网站的分布式均匀的。相反,从中国的情况看,整个网络形状不规则,内容相同的网站聚集程度更高,网站之间的异质性差异更大。比较印第安纳大学的S型曲线,我们发现中国网站中流量和耗散的累积曲线在前端上升得更加陡峭。这说明少数网站(百度、QQ、淘宝)占据了过多的流量。而小网站的流量更少,中国互联网企业垄断趋势更加的明显。

主要参考文献

注意力流网络与社区粘性

运用注意力流网络不仅可以研究整个互联网的整体情况,还可以研究某一个网站内部的注意力流分布。当我们选定了一段时间,选择了待研究的网站(网络社区)边界,我们就能用一个注意力流网络对网站(社区)进行建模。其中,每个节点都是一个网页(帖子、图像等数字资源),而节点之间依靠人类的接续点击行为相互组织形成了网络。这样,我们就可以用这个注意力流网络来对网站(社区)来进行抽象描述,网站(社区)的属性应该可以反映到注意力流网络之中。

交互粘性

例如,我们用百度贴吧的注意力流网络就可以考察网络贴吧的交互粘性。首先,我们需要指出,所谓的交互粘性就是指下面方程的指数[math]\displaystyle{ \theta }[/math]

[math]\displaystyle{ Y_t\propto X_t^{\theta} }[/math]

这里的[math]\displaystyle{ Y_t }[/math]为第t个小时内某百度贴吧的总点击量,[math]\displaystyle{ X_t }[/math]则为该小时内该贴吧的总独立访客数。通过分析大量的贴吧数据,我们发现,尽管贴吧彼此差异很大,但是它们都遵循上面的方程(如下图),其中[math]\displaystyle{ \theta }[/math]是一个只与贴吧有关,而与时间、规模无关的常数。

 

我们将指数[math]\displaystyle{ \theta }[/math]定义为该社区的交互粘性。这是因为[math]\displaystyle{ Y_t/X_t\propto X_t^{\theta-1} }[/math],也就是人均产生的点击量会随着用户的增多而增多(或减少,取决于[math]\displaystyle{ \theta }[/math]大于或小于1)。当[math]\displaystyle{ \theta }[/math]大的时候,人均点击量会随着访客数的增加而以更快的速度增长。采用这个指标衡量交互粘性具有一定的稳定性,这是因为[math]\displaystyle{ \theta }[/math]基本不随规模、时间而变。

耗散模式与交互粘性

那么,什么因素与[math]\displaystyle{ \theta }[/math]有关呢?我们研究发现,社区的注意力流网络的耗散模式与其正相关。首先,对于任意一个注意力流网络,我们发现了如下的关系:

[math]\displaystyle{ T_i\propto D_i^{\gamma} }[/math]

其中,T为每个帖子相应的流量,D为它的耗散流,也就是从i下网的总人次数。指数[math]\displaystyle{ \gamma }[/math]就反映了一个网络社区的耗散模式,它的数值越大,则流量大节点的耗散比率就越多(也就是有更大比例的用户会在大流量节点流失掉)。

我们进一步发现了指数[math]\displaystyle{ \theta }[/math]和指数[math]\displaystyle{ \gamma }[/math]之间的关系如下图所示:

 

由图可见,[math]\displaystyle{ \theta }[/math]会随着[math]\displaystyle{ \gamma }[/math]的增加而降低。所以,用户在一个网站上的流失模式会影响这个社区的交互粘性。 这个结论的直观说明是,如果一个贴吧充斥着大量的“标题党”帖子,那么它的交互粘性一定非常低。这是因为,在这样的贴吧中,用户点击这些标题贴会促使这些帖子的流量升高,但是由于[math]\displaystyle{ \gamma }[/math]的数值大,所以大流量节点相对流失率也高,所以这样的贴吧粘性就会低。

主要参考文献