MaxCompute 圖計算使用者手冊(上)

芊寶寶發表於2019-04-19

概要

ODPS GRAPH是一套面向迭代的圖計算處理框架。圖計算作業使用圖進行建模,圖由點(Vertex)和邊(Edge)組成,點和邊包含權值(Value),ODPS GRAPH支援下述圖編輯操作:

  • 修改點或邊的權值;
  • 增加/刪除點;
  • 增加/刪除邊;
備註:
  • 編輯點和邊時,點與邊的關係需要使用者維護。

通過迭代對圖進行編輯、演化,最終求解出結果,典型應用:PageRank單源最短距離演算法K-均值聚類演算法 等等。使用者可以使用 ODPS GRAPH 提供的介面Java SDK編寫圖計算程式。

[]()Graph資料結構

ODPS GRAPH能夠處理的圖必須是是一個由點(Vertex)和邊(Edge)組成的有向圖。由於ODPS僅提供二維表的儲存結構,因此需要使用者自行將圖資料分解為二維表格式儲存在ODPS中,在進行圖計算分析時,使用自定義的GraphLoader將二維表資料轉換為ODPS Graph引擎中的點和邊。至於如何將圖資料分解為二維表格式,使用者可以根據各自的業務場景做決定。在 示例程式 中,我們給出的示例分別使用不同的表格式來表達圖的資料結構,僅供大家參考。
點的結構可以簡單表示為 < ID, Value, Halted, Edges >,分別表示點識別符號(ID),權值(Value),狀態(Halted, 表示是否要停止迭代),出邊集合(Edges,以該點為起始點的所有邊列表)。邊的結構可以簡單表示為,分別表示目標點(DestVertexID)和權值(Value)。

MaxCompute 圖計算使用者手冊(上)

例如,上圖由下面的點組成:

Vertexv0<0, 0, false, [ <1, 5 >, <2, 10 > ] >v1<1, 5, false, [ <2, 3>, <3, 2>, <5, 9>]>v2<2, 8, false, [<1, 2>, <5, 1 >]>v3<3, Long.MAX_VALUE, false, [<0, 7>, <5, 6>]>v5<5, Long.MAX_VALUE, false, [<3, 4 > ]>

[]()Graph 程式邏輯

[]()1. 載入圖:

圖載入:框架呼叫使用者自定義的GraphLoader將輸入表的記錄解析為點或邊;分散式化:框架呼叫使用者自定義的Partitioner對點進行分片(預設分片邏輯:點ID雜湊值然後對Worker數取模),分配到相應的Worker;

MaxCompute 圖計算使用者手冊(上)

例如,上圖假設Worker數是2,那麼v0, v2會被分配到Worker0,因為ID對2取模結果為0,而v1, v3, v5將被分配到Worker1,ID對2取模結果為1;

[]()2. 迭代計算:

  • 一次迭代為一個”超步”(SuperStep),遍歷所有非結束狀態(Halted值為false)的點或者收到訊息的點(處於結束狀態的點收到資訊會被自動喚醒),並呼叫其compute(ComputeContext context, Iterable messages)方法;
  • 在使用者實現的compute(ComputeContext context, Iterable messages)方法中:
    • 處理上一個超步發給當前點的訊息(Messages);
    • 根據需要對圖進行編輯:1). 修改點/邊的取值;2). 傳送訊息給某些點;3). 增加/刪除點或邊;
    • 通過Aggregator彙總資訊到全域性資訊;
    • 設定當前點狀態,結束或非結束狀態;
    • 迭代進行過程中,框架會將訊息以非同步的方式傳送到對應Worker並在下一個超步進行處理,使用者無需關心;

[]()3. 迭代終止(滿足以下任意一條):

  • 所有點處於結束狀態(Halted值為true)且沒有新訊息產生;
  • 達到最大迭代次數;
  • 某個Aggregator的terminate方法返回true;

虛擬碼描述如下:

// 1. load
for each record in input_table {
  GraphLoader.load();
}
// 2. setup
WorkerComputer.setup();
for each aggr in aggregators {
  aggr.createStartupValue();
}
for each v in vertices {
  v.setup();
}
// 3. superstep
for (step = 0; step < max; step ++) {
  for each aggr in aggregators {
    aggr.createInitialValue();
  }
  for each v in vertices {
     v.compute();
   }
}
// 4. cleanup
for each v in vertices {
  v.cleanup();
}
WorkerComputer.cleanup();複製程式碼

Aggregator

Aggregator是ODPS-GRAPH作業中常用的feature之一,特別是解決機器學習問題時。ODPS-GRAPH中Aggregator用於彙總並處理全域性資訊。本文將詳細介紹的Aggregator的執行機制、相關API,並以Kmeans Clustering為例子說明Aggregator的具體用法。

Aggregator機制

Aggregator的邏輯分兩部分,一部分在所有Worker上執行,即分散式執行,另一部分只在AggregatorOwner所在Worker上執行,即單點。其中在所有Worker上執行的操作包括建立初始值及區域性聚合,然後將區域性聚合結果傳送給AggregatorOwner所在Worker上。AggregatorOwner所在Worker上聚合普通Worker傳送過來的區域性聚合物件,得到全域性聚合結果,然後判斷迭代是否結束。全域性聚合的結果會在下一輪超步分發給所有Worker,供下一輪迭代使用。 如下圖所示 :

MaxCompute 圖計算使用者手冊(上)

Aggregator的API

Aggregator共提供了五個API供使用者實現。下面逐個介紹5個API的呼叫時機及常規用途。

  1. createStartupValue(context)
    該API在所有Worker上執行一次,呼叫時機是所有超步開始之前,通常用以初始化AggregatorValue。在第0輪超步中,呼叫WorkerContext.getLastAggregatedValue() 或ComputeContext.getLastAggregatedValue()可以獲取該API初始化的AggregatorValue物件。
  2. createInitialValue(context)
    該API在所有Worker上每輪超步開始時呼叫一次,用以初始化本輪迭代所用的AggregatorValue。通常操作是通過WorkerContext.getLastAggregatedValue() 得到上一輪迭代的結果,然後執行部分初始化操作。
  3. aggregate(value, item)
    該API同樣在所有Worker上執行,與上述API不同的是,該API由使用者顯示呼叫ComputeContext#aggregate(item)來觸發,而上述兩個API,則由框架自動呼叫。該API用以執行區域性聚合操作,其中第一個引數value是本Worker在該輪超步已經聚合的結果(初始值是createInitialValue返回的物件),第二個引數是使用者程式碼呼叫ComputeContext#aggregate(item)傳入的引數。該API中通常用item來更新value實現聚合。所有aggregate執行完後,得到的value就是該Worker的區域性聚合結果,然後由框架傳送給AggregatorOwner所在的Worker。
  4. merge(value, partial)
    該API執行於AggregatorOwner所在Worker,用以合併各Worker區域性聚合的結果,達到全域性聚合物件。與aggregate類似,value是已經聚合的結果,而partial待聚合的物件,同樣用partial更新value。
    假定有3個worker,分別是w0、w1、w2,其區域性聚合結果是p0、p1、p2。假定傳送到AggregatorOwner所在Worker的順序為p1、p0、p2。那麼merge執行次序為,首先執行merge(p1, p0),這樣p1和p0就聚合為p1',然後執行merge(p1', p2),p1'和p2聚合為p1'',而p1''即為本輪超步全域性聚合的結果。
    從上述示例可以看出,當只有一個worker時,不需要執行merge方法,也就是說merge()不會被呼叫。

terminate(context, value)
當AggregatorOwner所在Worker執行完merge()後,框架會呼叫terminate(context, value)執行最後的處理。其中第二個引數value,即為merge()最後得到全域性聚合,在該方法中可以對全域性聚合繼續修改。執行完terminate()後,框架會將全域性聚合物件分發給所有Worker,供下一輪超步使用。
terminate()方法的一個特殊之處在於,如果返回true,則整個作業就結束迭代,否則繼續執行。在機器學習場景中,通常判斷收斂後返回true以結束作業。

Kmeans Clustering示例

下面以典型的KmeansClustering作為示例,來看下Aggregator具體用法。附件有完整程式碼,這裡我們逐個部分解析程式碼。

  1. GraphLoader部分
    GraphLoader部分用以載入輸入表,並轉換為圖的點或邊。這裡我們輸入表的每行資料為一個樣本,一個樣本構造一個點,並用Vertex的value來存放樣本。
    我們首先定義一個Writable類KmeansValue作為Vertex的value型別:
    `java
  2. static class KmeansValue implements Writable {

DenseVector sample;
public KmeansValue() {
}
public KmeansValue(DenseVector v) {
this.sample = v;
}
@Override
public void write(DataOutput out) throws IOException {
wirteForDenseVector(out, sample);
}
@Override
public void readFields(DataInput in) throws IOException {
sample = readFieldsForDenseVector(in);
}
}

KmeansValue中封裝一個DenseVector物件來存放一個樣本,這裡DenseVector型別來自[matrix-toolkits-java](https://github.com/fommil/matrix-toolkits-java/),而wirteForDenseVector()及readFieldsForDenseVector()用以實現序列化及反序列化,可參見附件中的完整程式碼。<br />我們自定義的KmeansReader程式碼如下:<br />```java
public static class KmeansReader extends 
 GraphLoader<LongWritable, KmeansValue, NullWritable, NullWritable> {
 @Override
 public void load(
     LongWritable recordNum,
     WritableRecord record,
     MutationContext<LongWritable, KmeansValue, NullWritable, NullWritable> context)
     throws IOException {
   KmeansVertex v = new KmeansVertex();
   v.setId(recordNum);
   int n = record.size();
   DenseVector dv = new DenseVector(n);
   for (int i = 0; i < n; i++) {
     dv.set(i, ((DoubleWritable)record.get(i)).get());
   }
   v.setValue(new KmeansValue(dv));
   context.addVertexRequest(v);
 }
}複製程式碼

KmeansReader中,每讀入一行資料(一個Record)建立一個點,這裡用recordNum作為點的ID,將record內容轉換成DenseVector物件並封裝進VertexValue中。

  1. Vertex部分
    自定義的KmeansVertex程式碼如下。邏輯非常簡單,每輪迭代要做的事情就是將自己維護的樣本執行區域性聚合。具體邏輯參見下面Aggregator的實現。
    `java
  2. static class KmeansVertex extends

Vertex {
@Override
public void compute(

ComputeContext<LongWritable, KmeansValue, NullWritable, NullWritable> context,
 Iterable<NullWritable> messages) throws IOException {複製程式碼

context.aggregate(getValue());
}
}

1. Aggregator部分<br />整個Kmeans的主要邏輯集中在Aggregator中。首先是自定義的KmeansAggrValue,用以維護要聚合及分發的內容。<br />```java
public static class KmeansAggrValue implements Writable {
 DenseMatrix centroids;
 DenseMatrix sums; // used to recalculate new centroids
 DenseVector counts; // used to recalculate new centroids
 @Override
 public void write(DataOutput out) throws IOException {
   wirteForDenseDenseMatrix(out, centroids);
   wirteForDenseDenseMatrix(out, sums);
   wirteForDenseVector(out, counts);
 }
 @Override
 public void readFields(DataInput in) throws IOException {
   centroids = readFieldsForDenseMatrix(in);
   sums = readFieldsForDenseMatrix(in);
   counts = readFieldsForDenseVector(in);
 }
}複製程式碼

KmeansAggrValue中維護了三個物件,其中centroids是當前的K箇中心點,假定樣本是m維的話,centroids就是一個K*m的矩陣。sums是和centroids大小一樣的矩陣,每個元素記錄了到特定中心點最近的樣本特定維之和,例如sums(i,j)是到第i箇中心點最近的樣本的第j維度之和。
counts是個K維的向量,記錄到每個中心點距離最短的樣本個數。sums和counts一起用以計算新的中心點,也是要聚合的主要內容。 接下來是自定義的Aggregator實現類KmeansAggregator,我們按照上述API的順序逐個看其實現。
首先是createStartupValue()。
`java
public static class KmeansAggregator extends Aggregator {
public KmeansAggrValue createStartupValue(WorkerContext context) throws IOException {
KmeansAggrValue av = new KmeansAggrValue();
byte[] centers = context.readCacheFile("centers");
String lines[] = new String(centers).split("n");
int rows = lines.length;
int cols = lines[0].split(",").length; // assumption rows >= 1
av.centroids = new DenseMatrix(rows, cols);
av.sums = new DenseMatrix(rows, cols);
av.sums.zero();
av.counts = new DenseVector(rows);
av.counts.zero();
for (int i = 0; i < lines.length; i++) {

String[] ss = lines[i].split(",");
 for (int j = 0; j < ss.length; j++) {
   av.centroids.set(i, j, Double.valueOf(ss[j]));
 }複製程式碼

}
return av;
}

我們在該方法中初始化一個KmeansAggrValue物件,然後從資原始檔centers中讀取初始中心點,並賦值給centroids。而sums和counts初始化為0。<br />接來下是createInitialValue()的實現:<br />```java
@Override
 public void aggregate(KmeansAggrValue value, Object item)
     throws IOException {
   DenseVector sample = ((KmeansValue)item).sample;
   // find the nearest centroid
   int min = findNearestCentroid(value.centroids, sample);
   // update sum and count
   for (int i = 0; i < sample.size(); i ++) {
     value.sums.add(min, i, sample.get(i));
   }
   value.counts.add(min, 1.0d);
 }複製程式碼

該方法中呼叫findNearestCentroid()(實現見附件)找到樣本item尤拉距離最近的中心點索引,然後將其各個維度加到sums上,最後counts計數加1。
以上三個方法執行於所有worker上,實現區域性聚合。接下來看下在AggregatorOwner所在Worker執行的全域性聚合相關操作。
首先是merge的實現:
`java
@Override
public void merge(KmeansAggrValue value, KmeansAggrValue partial)

throws IOException {複製程式碼

value.sums.add(partial.sums);
value.counts.add(partial.counts);
}

merge的實現邏輯很簡單,就是把各個worker聚合出的sums和counts相加即可。<br />最後是terminate()的實現:<br />```java
@Override
 public boolean terminate(WorkerContext context, KmeansAggrValue value)
     throws IOException {
   // Calculate the new means to be the centroids (original sums)
   DenseMatrix newCentriods = calculateNewCentroids(value.sums, value.counts, value.centroids);
   // print old centroids and new centroids for debugging
   System.out.println("\nsuperstep: " + context.getSuperstep() + 
       "\nold centriod:\n" + value.centroids + " new centriod:\n" + newCentriods);
   boolean converged = isConverged(newCentriods, value.centroids, 0.05d);
   System.out.println("superstep: " + context.getSuperstep() + "/" 
       + (context.getMaxIteration() - 1) + " converged: " + converged);
   if (converged || context.getSuperstep() == context.getMaxIteration() - 1) {
     // converged or reach max iteration, output centriods
     for (int i = 0; i < newCentriods.numRows(); i++) {
       Writable[] centriod = new Writable[newCentriods.numColumns()];
       for (int j = 0; j < newCentriods.numColumns(); j++) {
         centriod[j] = new DoubleWritable(newCentriods.get(i, j));
       }
       context.write(centriod);
     }
     // true means to terminate iteration
     return true;
   }
   // update centriods
   value.centroids.set(newCentriods);
   // false means to continue iteration
   return false;
 }複製程式碼

teminate()中首先根據sums和counts呼叫calculateNewCentroids()求平均計算出新的中心點。然後呼叫isConverged()根據新老中心點尤拉距離判斷是否已經收斂。如果收斂或迭代次數達到最大數,則將新的中心點輸出並返回true,以結束迭代。否則更新中心點並返回false以繼續迭代。其中calculateNewCentroids()和isConverged()的實現見附件。

  1. main方法
    main方法用以構造GraphJob,然後設定相應配置,並提交作業。程式碼如下:
    `java
  2. static void main(String[] args) throws IOException {

if (args.length < 2)
printUsage();
GraphJob job = new GraphJob();
job.setGraphLoaderClass(KmeansReader.class);
job.setRuntimePartitioning(false);
job.setVertexClass(KmeansVertex.class);
job.setAggregatorClass(KmeansAggregator.class);
job.addInput(TableInfo.builder().tableName(args[0]).build());
job.addOutput(TableInfo.builder().tableName(args[1]).build());
// default max iteration is 30
job.setMaxIteration(30);
if (args.length >= 3)
job.setMaxIteration(Integer.parseInt(args[2]));
long start = System.currentTimeMillis();
job.run();
System.out.println("Job Finished in "

+ (System.currentTimeMillis() - start) / 1000.0 + " seconds");複製程式碼

}

這裡需要注意的是job.setRuntimePartitioning(false),設定為false後,各個worker載入的資料不再根據Partitioner重新分割槽,即誰載入的資料誰維護。

<a name="a7d80080"></a>
# 功能介紹
<a name="1922c3a4"></a>
## 執行作業
MaxCompute 客戶端提供一個Jar命令用於執行 MaxCompute GRAPH作業,其使用方式與 [MapReduce](http://help.aliyun-inc.com/internaldoc/detail/27875.html)中的[Jar命令](http://help.aliyun-inc.com/internaldoc/detail/27878.html) 相同,這裡僅作簡要介紹:複製程式碼

Usage: jar [] [ARGS]

-conf <configuration_file>         Specify an application configuration file
-classpath <local_file_list>       classpaths used to run mainClass
-D <name>=<value>                  Property value pair, which will be used to run mainClass
-local                             Run job in local mode
-resources <resource_name_list>    file/table resources used in graph, seperate by comma
其中 < GENERIC_OPTIONS>包括(均為可選引數):
* -conf <configuration file > :指定JobConf配置檔案;
* -classpath <local_file_list > : 本地執行時的classpath,主要用於指定main函式所在的jar包。大多數情況下,使用者更習慣於將main函式與Graph作業編寫在一個包中,例如:單源最短距離演算法 ,因此,在執行示例程式時,-resources及-classpath的引數中都出現了使用者的jar包,但二者意義不同,-resources引用的是Graph作業,執行於分散式環境中,而-classpath引用的是main函式,執行於本地,指定的jar包路徑也是本地檔案路徑。包名之間使用系統預設的檔案分割符作分割(通常情況下,windows系統是分號”;”,linux系統是冒號”:”);
* -D <prop_name > = < prop_value > : 本地執行時,<mainClass > 的java屬性,可以定義多個;
* -local:以本地模式執行Graph作業,主要用於程式除錯;
* -resources <resource_name_list > : Graph作業執行時使用的資源宣告。一般情況下,resource_name_list中需要指定Graph作業所在的資源名稱。如果使用者在Graph作業中讀取了其他ODPS資源,那麼,這些資源名稱也需要被新增到resource_name_list中。資源之間使用逗號分隔,使用跨專案空間使用資源時,需要前面加上:PROJECT_NAME/resources/,示例:-resources otherproject/resources/resfile;

同時,使用者也可以直接執行GRAPH作業的main函式直接將作業提交到 MaxCompute ,而不是通過 MaxCompute 客戶端提交作業。以[PageRank演算法](http://help.aliyun-inc.com/internaldoc/detail/27908.html) 為例:複製程式碼

public static void main(String[] args) throws IOException {
if (args.length < 2)

printUsage();複製程式碼

GraphJob job = new GraphJob();
job.setGraphLoaderClass(PageRankVertexReader.class);
job.setVertexClass(PageRankVertex.class);
job.addInput(TableInfo.builder().tableName(args[0]).build());
job.addOutput(TableInfo.builder().tableName(args[1]).build());
// 將作業中使用的資源新增到cache resource,對應於jar命令中 -resources 和 -libjars 中指定的資源
job.addCacheResource("mapreduce-examples.jar");
// 將使用的jar及其他檔案新增到class cache resource,對應於jar命令中 -libjars 中指定的資源
job.addCacheResourceToClassPath("mapreduce-examples.jar");
// 設定console中,odps_config.ini對應的配置項,使用時替換為自己的配置
OdpsConf.getInstance().setProjName("project_name");
OdpsConf.getInstance().setEndpoint("end_point");
OdpsConf.getInstance().setAccessId("access_id");
OdpsConf.getInstance().setAccessKey("access_key");
// default max iteration is 30
job.setMaxIteration(30);
if (args.length >= 3)

job.setMaxIteration(Integer.parseInt(args[2]));複製程式碼

long startTime = System.currentTimeMillis();
job.run();
System.out.println("Job Finished in "

+ (System.currentTimeMillis() - startTime) / 1000.0 + " seconds");複製程式碼

}

<a name="6354d6d6"></a>
## []()輸入輸出
MaxCompute GRAPH作業的輸入輸出限制為表,不允許使用者自定義輸入輸出格式。<br />定義作業輸入,支援多路輸入:複製程式碼

GraphJob job = new GraphJob();
job.addInput(TableInfo.builder().tableName(“tblname”).build()); //表作為輸入
job.addInput(TableInfo.builder().tableName(“tblname”).partSpec("pt1=a/pt2=b").build()); //分割槽作為輸入
//只讀取輸入表的 col2 和 col0 列,在 GraphLoader 的 load 方法中,record.get(0) 得到的是col2列,順序一致
job.addInput(TableInfo.builder().tableName(“tblname”).partSpec("pt1=a/pt2=b").build(), new String[]{"col2", "col0"});
備註:

關於作業輸入定義,更多的資訊參見GraphJob的addInput相關方法說明,框架讀取輸入表的記錄傳給使用者自定義的GraphLoader載入圖資料;
限制: 暫時不支援分割槽過濾條件。更多應用限制請參考 應用限制;
定義作業輸出,支援多路輸出,通過label標識每路輸出:

GraphJob job = new GraphJob();
//輸出表為分割槽表時需要給到最末一級分割槽
job.addOutput(TableInfo.builder().tableName("table_name").partSpec("pt1=a/pt2=b").build());
// 下面的引數 true 表示覆蓋tableinfo指定的分割槽,即INSERT OVERWRITE語義,false表示INSERT INTO語義
job.addOutput(TableInfo.builder().tableName("table_name").partSpec("pt1=a/pt2=b").lable("output1").build(), true);

> 備註:
> * 關於作業輸出定義,更多的資訊參見GraphJob的addOutput 相關方法說明;
* Graph作業在執行時可以通過WorkerContext的write方法寫出記錄到輸出表,多路輸出需要指定標識,如上面的 “output1”;
* 更多應用限制請參考 [應用限制](http://help.aliyun-inc.com/internaldoc/detail/27905.html);

<a name="d41d8cd9"></a>
# 
<a name="bbfcdb67"></a>
## 讀取資源
<a name="24cb2794"></a>
### []()GRAPH程式中新增資源
除了通過jar命令指定GRAPH讀取的資源外,還可以通過GraphJob的下面兩個方法指定:複製程式碼

void addCacheResources(String resourceNames)
void addCacheResourcesToClassPath(String resourceNames)

<a name="90d49894"></a>
### []()GRAPH程式中使用資源
在 GRAPH 程式中可以通過相應的上下文物件WorkerContext的下述方法讀取資源:複製程式碼

public byte[] readCacheFile(String resourceName) throws IOException;
public Iterable readCacheArchive(String resourceName) throws IOException;
public Iterable readCacheArchive(String resourceName, String relativePath)throws IOException;
public Iterable readResourceTable(String resourceName);
public BufferedInputStream readCacheFileAsStream(String resourceName) throws IOException;
public Iterable readCacheArchiveAsStream(String resourceName) throws IOException;
public Iterable readCacheArchiveAsStream(String resourceName, String relativePath) throws IOException;

> 備註:
> * 通常在WorkerComputer的setup方法裡讀取資源,然後儲存在Worker Value中,之後通過getWorkerValue方法取得;
* 建議用上面的流介面,邊讀邊處理,記憶體耗費少;
* 更多應用限制請參考 [應用限制](http://help.aliyun-inc.com/internaldoc/detail/27905.html);


<a name="5f839cd3"></a>
# SDK介紹
Graph SDK maven 配置:複製程式碼


com.aliyun.odps
odps-sdk-graph
0.20.7
sources

完整Java Doc文件,請點選 [這裡](http://odps.alibaba-inc.com/doc/prddoc/odps_sdk_v2/apidocs/index.html)

| 主要介面 | 說明 |
| :--- | :--- |
| GraphJob | GraphJob繼承自JobConf,用於定義、提交和管理一個 ODPS Graph 作業。 |
| Vertex | Vertex是圖的點的抽象,包含屬性:id,value,halted,edges,通過GraphJob的setVertexClass介面提供 Vertex 實現。 |
| Edge | Edge是圖的邊的抽象,包含屬性:destVertexId, value,圖資料結構採用鄰接表,點的出邊儲存在點的 edges 中。 |
| GraphLoader | GraphLoader用於載入圖,通過 GraphJob 的 setGraphLoaderClass 介面提供 GraphLoader 實現。 |
| VertexResolver | VertexResolver用於自定義圖拓撲修改時的衝突處理邏輯,通過GraphJob的 setLoadingVertexResolverClass 和 setComputingVertexResolverClass 介面提供圖載入和迭代計算過程中的圖拓撲修改的衝突處理邏輯。 |
| Partitioner | Partitioner 用於對圖進行劃分使得計算可以分片進行,通過GraphJob的 setPartitionerClass 介面提供 Partitioner 實現,預設採用 HashPartitioner,即對點 ID 求雜湊值然後對 Worker 數目取模。 |
| WorkerComputer | WorkerComputer允許在 Worker 開始和退出時執行使用者自定義的邏輯,通過GraphJob的 setWorkerComputerClass 介面提供WorkerComputer 實現。 |
| Aggregator | Aggregator 的 setAggregatorClass(Class ...) 定義一個或多個 Aggregator |
| Combiner | Combiner 的 setCombinerClass 設定 Combiner |
| Counters | 計數器,在作業執行邏輯中,可以通過 WorkerContext 介面取得計數器並進行計數,框架會自動進行彙總 |
| WorkerContext | 上下文物件,封裝了框架的提供的功能,如修改圖拓撲結構,傳送訊息,寫結果,讀取資源等等 |

<a name="8e705b7e"></a>
# 開發和除錯
ODPS沒有為使用者提供Graph開發外掛,但使用者仍然可以基於Eclipse開發ODPS Graph程式,建議的開發流程是:
* 編寫Graph程式碼,使用本地除錯進行基本的測試;
* 進行叢集除錯,驗證結果;
<a name="9973bec7"></a>
## 開發示例
本節以[SSSP](http://help.aliyun-inc.com/internaldoc/detail/27907.html) 演算法為例講述如何用Eclipse開發和除錯Graph程式。<br />下面是開發SSSP的步驟:
1. 建立Java工程,例如:graph_examples;<br />
1. 將ODPS客戶端lib目錄下的jar包加到Eclipse工程的Build Path裡。一個配置好的Eclipse工程如下圖所示。<br />
1. 開發ODPS Graph程式,實際開發過程中,常常會先拷貝一個例子(例如[SSSP](http://help.aliyun-inc.com/internaldoc/detail/27907.html)),然後再做修改。在本示例中,我們僅修改了package路徑為:package com.aliyun.odps.graph.example。
1. 編譯打包,在Eclipse環境中,右鍵點選原始碼目錄(圖中的src目錄),Export -> Java -> JAR file 生成JAR包,選擇目標jar包的儲存路徑,例如:D:\odps\clt\odps-graph-example-sssp.jar;
1. 使用ODPS客戶端執行SSSP,相關操作參考[快速開始之執行Graph](http://help.aliyun-inc.com/internaldoc/detail/27813.html)。

Eclipse 配置截圖: ![](https://intranetproxy.alipay.com/skylark/lark/0/2019/png/23934/1548150832059-15fe7b48-5b7f-45b9-b9fd-5d8dd2014732.png#align=left&display=inline&height=448&originHeight=770&originWidth=1281&size=0&width=746)
> 注意:
> * 相關的開發步驟請參考[Graph開發外掛介紹](http://help.aliyun-inc.com/internaldoc/detail/27985.html).

<a name="8f6be038"></a>
## 本地除錯
ODPS GRAPH支援本地除錯模式,可以使用Eclipse進行斷點除錯。<br />斷點除錯步驟如下:
* 下載一個odps-graph-local的maven包。
* 選擇Eclipse工程,右鍵點選GRAPH作業主程式(包含main函式)檔案,配置其執行引數(Run As -> Run Configurations…),如下圖。
* 在Arguments tab頁中,設定Program arguments 引數為“1 sssp_in sssp_out”,作為主程式的輸入引數;
* 在Arguments tab頁中,設定VM arguments引數為:<br />-Dodps.runner.mode=local -Dodps.project.name=<project.name> -Dodps.end.point=<end.point> -Dodps.access.id=<access.id> -Dodps.access.key=<access.key>

![](https://intranetproxy.alipay.com/skylark/lark/0/2019/png/23934/1548150832059-b6f956c0-ecdc-4de9-9338-3ff9e9305261.png#align=left&display=inline&height=597&originHeight=640&originWidth=800&size=0&width=746)
* 對於本地模式(即odps.end.point引數不指定),需要在warehouse建立sssp_in,sssp_out表,為輸入表 sssp_in 新增資料,輸入資料如下。關於warehouse的介紹請參考[MapReduce本地執行](http://help.aliyun-inc.com/internaldoc/detail/27882.html) 部分;複製程式碼

1,"2:2,3:1,4:4"
2,"1:2,3:2,4:1"
3,"1:1,2:2,5:1"
4,"1:4,2:1,5:1"
5,"3:1,4:1"

* 點選Run按鈕即可本地跑SSSP;

其中:引數設定可參考ODPS客戶端中conf/odps_config.ini的設定,上述是幾個常用引數,其他引數也說明如下:
* odps.runner.mode:取值為local,本地除錯功能必須指定;
* odps.project.name:指定當前project,必須指定;
* odps.end.point:指定當前odps服務的地址,可以不指定,如果不指定,只從warehouse讀取表或資源的meta和資料,不存在則拋異常,如果指定,會先從warehouse讀取,不存在時會遠端連線odps讀取;
* odps.access.id:連線odps服務的id,只在指定odps.end.point時有效;
* odps.access.key:連線odps服務的key,只在指定odps.end.point時有效;
* odps.cache.resources:指定使用的資源列表,效果與jar命令的“-resources”相同;
* odps.local.warehouse: 本地warehouse路徑,不指定時預設為./warehouse;

在 Eclipse 中本地跑 SSSP的除錯輸出資訊如下:複製程式碼

Counters: 3

com.aliyun.odps.graph.local.COUNTER
             TASK_INPUT_BYTE=211
             TASK_INPUT_RECORD=5
             TASK_OUTPUT_BYTE=161
             TASK_OUTPUT_RECORD=5複製程式碼

graph task finish

> 注意:在上面的示例中,需要本地warehouse下有sssp_in及sssp_out表。sssp_in及sssp_out的詳細資訊請參考[快速開始之執行Graph](http://help.aliyun-inc.com/internaldoc/detail/27813.html)中的介紹。

<a name="035a3e86"></a>
## 本地作業臨時目錄
每執行一次本地除錯,都會在 Eclipse 工程目錄下新建一個臨時目錄,見下圖:<br />![](https://intranetproxy.alipay.com/skylark/lark/0/2019/png/23934/1548150832063-a27eee32-76f2-496b-9e7c-4aa622ebf33d.png#align=left&display=inline&height=199&originHeight=199&originWidth=272&size=0&width=272)<br />一個本地執行的GRAPH作業臨時目錄包括以下幾個目錄和檔案:
* counters - 存放作業執行的一些計數資訊;
* inputs - 存放作業的輸入資料,優先取自本地的 warehouse,如果本地沒有,會通過 ODPS SDK 從服務端讀取(如果設定了 odps.end.point),預設一個 input 只讀10 條資料,可以通過 -Dodps.mapred.local.record.limit 引數進行修改,但是也不能超過1萬條記錄;
* outputs - 存放作業的輸出資料,如果本地warehouse中存在輸出表,outputs裡的結果資料在作業執行完後會覆蓋本地warehouse中對應的表;
* resources - 存放作業使用的資源,與輸入類似,優先取自本地的warehouse,如果本地沒有,會通過ODPS SDK從服務端讀取(如果設定了 odps.end.point);
* job.xml - 作業配置
* superstep - 存放每一輪迭代的訊息持久化資訊。> 注意:
> * 如果需要本地除錯時輸出詳細日誌,需要在 src 目錄下放一個 log4j 的配置檔案:log4j.properties_odps_graph_cluster_debug。


<a name="f949cc7a"></a>
## 叢集除錯
在通過本地的除錯之後,可以提交作業到叢集進行測試,通常步驟:
1. 配置ODPS客戶端;
1. 使用“add jar /path/work.jar -f;”命令更新jar包;
1. 使用jar命令執行作業,檢視執行日誌和結果資料,如下所示;
> 注意:
> * 叢集執行Graph的詳細介紹可以參考[快速開始之執行Graph](http://help.aliyun-inc.com/internaldoc/detail/27813.html)。

<a name="5865fdf5"></a>
## 效能調優
下面主要從 ODPS Graph 框架角度介紹常見效能優化的幾個方面:
<a name="5c70b720"></a>
## 作業引數配置
對效能有所影響的 GraphJob 配置項包括:
* setSplitSize(long) // 輸入表切分大小,單位MB,大於0,預設64;
* setNumWorkers(int) // 設定作業worker數量,範圍:[1, 1000], 預設值-1, worker數由作業輸入位元組數和split size決定;
* setWorkerCPU(int) // Map CPU資源,100為1cpu核,[50,800]之間,預設200;
* setWorkerMemory(int) // Map 記憶體資源,單位MB,[256M,12G]之間,預設4096M;
* setMaxIteration(int) // 設定最大迭代次數,預設 -1,小於或等於 0 時表示最大迭代次數不作為作業終止條件;
* setJobPriority(int) // 設定作業優先順序,範圍:[0, 9],預設9,數值越大優先順序越小。

通常情況下:
1. 可以考慮使用setNumWorkers方法增加 worker 數目;
1. 可以考慮使用setSplitSize方法減少切分大小,提高作業載入資料速度;
1. 加大 worker 的 cpu 或記憶體;
1. 設定最大迭代次數,有些應用如果結果精度要求不高,可以考慮減少迭代次數,儘快結束;

介面 setNumWorkers 與 setSplitSize 配合使用,可以提高資料的載入速度。假設 setNumWorkers 為 workerNum, setSplitSize 為 splitSize, 總輸入位元組數為 inputSize, 則輸入被切分後的塊數 splitNum = inputSize / splitSize,workerNum 和 splitNum 之間的關係:
1. 若 splitNum == workerNum,每個 worker 負責載入一個 split;
1. 若 splitNum > workerNum,每個 worker 負責載入一個或多個 split;
1. 若 splitNum < workerNum, 每個 worker 負責載入零個或一個 split。

因此,應調節 workerNum 和 splitSize,在滿足前兩種情況時,資料載入比較快。迭代階段只調節 workerNum 即可。 如果設定 runtime partitioning 為 false,則建議直接使用 setSplitSize 控制 worker 數量,或者保證滿足前兩種情況,在出現第三種情況時,部分 worker 上點數會為0. 可以在 jar 命令前使用set odps.graph.split.size=<m>; set odps.graph.worker.num=<n>; 與 setNumWorkers 和 setSplitSize 等效。<br />另外一種常見的效能問題:資料傾斜,反應到 Counters 就是某些 worker 處理的點或邊數量遠遠超過其他 worker。<br />資料傾斜的原因通常是某些 key 對應的點、邊,或者訊息的數量遠遠超出其他 key,這些 key 被分到少量的 worker 處理,從而導致這些 worker 相對於其他執行時間長很多,解決方法:
* 可以試試 Combiner,把這些 key 對應點的訊息進行本地聚合,減少訊息發生;
* 改進業務邏輯。
<a name="6bb8613c"></a>
## 運用Combiner
開發人員可定義 Combiner 來減少儲存訊息的記憶體和網路資料流量,縮短作業的執行時間。細節見 SDK中Combiner的介紹。
<a name="654376db"></a>
## 減少資料輸入量
資料量大時,讀取磁碟中的資料可能耗費一部分處理時間,因此,減少需要讀取的資料位元組數可以提高總體的吞吐量,從而提高作業效能。可供選擇的方法有如下幾種:
* 減少輸入資料量:對某些決策性質的應用,處理資料取樣後子集所得到的結果只可能影響結果的精度,而並不會影響整體的準確性,因此可以考慮先對資料進行特定取樣後再匯入輸入表中進行處理
* 避免讀取用不到的欄位:ODPS Graph 框架的 TableInfo 類支援讀取指定的列(以列名陣列方式傳入),而非整個表或表分割槽,這樣也可以減少輸入的資料量,提高作業效能
<a name="e3f29de3"></a>
## 內建jar包
下面這些 jar 包會預設載入到執行 GRAPH 程式的 JVM 中,使用者可以不必上傳這些資源,也不必在命令列的 -libjars 帶上這些 jar 包:
* commons-codec-1.3.jar
* commons-io-2.0.1.jar
* commons-lang-2.5.jar
* commons-logging-1.0.4.jar
* commons-logging-api-1.0.4.jar
* guava-14.0.jar
* json.jar
* log4j-1.2.15.jar
* slf4j-api-1.4.3.jar
* slf4j-log4j12-1.4.3.jar
* xmlenc-0.52.jar
> 注意:
> * 在起 JVM 的CLASSPATH 裡,上述內建 jar 包會放在使用者 jar 包的前面,所以可能產生版本衝突,例如:使用者的程式中使用了 commons-codec-1.5.jar 某個類的函式,但是這個函式不在 commons-codec-1.3.jar 中,這時只能看 1.3 版本里是否有滿足你需求的實現,或者等待ODPS升級新版本。


<a name="babfb10e"></a>
# 應用限制
* 單個job引用的resource數量不超過256個,table、archive按照一個單位計算;
* 單個job引用的resource總計位元組數大小不超過512M;
* 單個job的輸入路數不能超過1024(輸入表的個數不能超過64),單個job的輸出路數不能超過256;
* 多路輸出中指定的label不能為null或者為空字串,長度不能超過256,只能包括A-Z,a-z,0-9,_,#,.,-等;
* 單個job中自定義counter的數量不能超過64,counter的group name和counter name中不能帶有#,兩者長度和不能超過100;
* 單個job的worker數由框架計算得出,最大為 1000, 超過拋異常;
* 單個worker佔用cpu預設為200,範圍[50, 800];
* 單個worker佔用memory預設為4096,範圍[256M, 12G];
* 單個worker重複讀一個resource次數限制不大於64次;
* plit size預設為64M,使用者可設定,範圍:0 < split_size <= (9223372036854775807 >> 20);
* ODPS Graph程式中的GraphLoader/Vertex/Aggregator等在叢集執行時,受到Java沙箱的限制(Graph作業的主程式則不受此限制),具體限制如 [Java沙箱](http://help.aliyun-inc.com/internaldoc/detail/34631.html) 所示。

<a name="3e49d1b2"></a>
# 示例程式
<a name="f56cb8a8"></a>
## 單源最短距離
Dijkstra 演算法是求解有向圖中單源最短距離(Single Source Shortest Path,簡稱為 SSSP)的經典演算法。<br />最短距離:對一個有權重的有向圖 G=(V,E),從一個源點 s 到匯點 v 有很多路徑,其中邊權和最小的路徑,稱從 s 到 v 的最短距離。<br />演算法基本原理,如下所示:
* 初始化:源點 s 到 s 自身的距離(d[s]=0),其他點 u 到 s 的距離為無窮(d[u]=∞)。<br />
* 迭代:若存在一條從 u 到 v 的邊,那麼從 s 到 v 的最短距離更新為:d[v]=min(d[v], d[u]+weight(u, v)),直到所有的點到 s 的距離不再發生變化時,迭代結束。<br />

由演算法基本原理可以看出,此演算法非常適合使用 MaxCompute Graph 程式進行求解:每個點維護到源點的當前最短距離值,當這個值變化時,將新值加上邊的權值傳送訊息通知其鄰接點,下一輪迭代時,鄰接點根據收到的訊息更新其當前最短距離,當所有點當前最短距離不再變化時,迭代結束。
<a name="b5ea48ff"></a>
### []()程式碼示例
單源最短距離的程式碼,如下所示:複製程式碼

import java.io.IOException;
import com.aliyun.odps.io.WritableRecord;
import com.aliyun.odps.graph.Combiner;
import com.aliyun.odps.graph.ComputeContext;
import com.aliyun.odps.graph.Edge;
import com.aliyun.odps.graph.GraphJob;
import com.aliyun.odps.graph.GraphLoader;
import com.aliyun.odps.graph.MutationContext;
import com.aliyun.odps.graph.Vertex;
import com.aliyun.odps.graph.WorkerContext;
import com.aliyun.odps.io.LongWritable;
import com.aliyun.odps.data.TableInfo;
public class SSSP {
public static final String START_VERTEX = "sssp.start.vertex.id";
public static class SSSPVertex extends

Vertex<LongWritable, LongWritable, LongWritable, LongWritable> {
private static long startVertexId = -1;
public SSSPVertex() {
  this.setValue(new LongWritable(Long.MAX_VALUE));
}
public boolean isStartVertex(
    ComputeContext<LongWritable, LongWritable, LongWritable, LongWritable> context) {
  if (startVertexId == -1) {
    String s = context.getConfiguration().get(START_VERTEX);
    startVertexId = Long.parseLong(s);
  }
  return getId().get() == startVertexId;
}
@Override
public void compute(
    ComputeContext<LongWritable, LongWritable, LongWritable, LongWritable> context,
    Iterable<LongWritable> messages) throws IOException {
  long minDist = isStartVertex(context) ? 0 : Integer.MAX_VALUE;
  for (LongWritable msg : messages) {
    if (msg.get() < minDist) {
      minDist = msg.get();
    }
  }
  if (minDist < this.getValue().get()) {
    this.setValue(new LongWritable(minDist));
    if (hasEdges()) {
      for (Edge<LongWritable, LongWritable> e : this.getEdges()) {
        context.sendMessage(e.getDestVertexId(), new LongWritable(minDist
            + e.getValue().get()));
      }
    }
  } else {
    voteToHalt();
  }
}
@Override
public void cleanup(
    WorkerContext<LongWritable, LongWritable, LongWritable, LongWritable> context)
    throws IOException {
  context.write(getId(), getValue());
}複製程式碼

}
public static class MinLongCombiner extends

Combiner<LongWritable, LongWritable> {
@Override
public void combine(LongWritable vertexId, LongWritable combinedMessage,
    LongWritable messageToCombine) throws IOException {
  if (combinedMessage.get() > messageToCombine.get()) {
    combinedMessage.set(messageToCombine.get());
  }
}複製程式碼

}
public static class SSSPVertexReader extends

GraphLoader<LongWritable, LongWritable, LongWritable, LongWritable> {
@Override
public void load(
    LongWritable recordNum,
    WritableRecord record,
    MutationContext<LongWritable, LongWritable, LongWritable, LongWritable> context)
    throws IOException {
  SSSPVertex vertex = new SSSPVertex();
  vertex.setId((LongWritable) record.get(0));
  String[] edges = record.get(1).toString().split(",");
  for (int i = 0; i < edges.length; i++) {
    String[] ss = edges[i].split(":");
    vertex.addEdge(new LongWritable(Long.parseLong(ss[0])),
        new LongWritable(Long.parseLong(ss[1])));
  }
  context.addVertexRequest(vertex);
}複製程式碼

}
public static void main(String[] args) throws IOException {

if (args.length < 2) {
  System.out.println("Usage: <startnode> <input> <output>");
  System.exit(-1);
}
GraphJob job = new GraphJob();
job.setGraphLoaderClass(SSSPVertexReader.class);
job.setVertexClass(SSSPVertex.class);
job.setCombinerClass(MinLongCombiner.class);
job.set(START_VERTEX, args[0]);
job.addInput(TableInfo.builder().tableName(args[1]).build());
job.addOutput(TableInfo.builder().tableName(args[2]).build());
long startTime = System.currentTimeMillis();
job.run();
System.out.println("Job Finished in "
    + (System.currentTimeMillis() - startTime) / 1000.0 + " seconds");複製程式碼

}
}

上述程式碼,說明如下:
* 第 19 行:定義 SSSPVertex ,其中:
  * 點值表示該點到源點 startVertexId 的當前最短距離。<br />
  * compute() 方法使用迭代公式:d[v]=min(d[v], d[u]+weight(u, v)) 更新點值。<br />
  * cleanup() 方法把點及其到源點的最短距離寫到結果表中。<br />
* 第 58 行:當點值沒發生變化時,呼叫 voteToHalt() 告訴框架該點進入 halt 狀態,當所有點都進入 halt 狀態時,計算結束。<br />
* 第 70 行:定義 MinLongCombiner,對傳送給同一個點的訊息進行合併,優化效能,減少記憶體佔用。<br />
* 第 83 行:定義 SSSPVertexReader 類,載入圖,將表中每一條記錄解析為一個點,記錄的第一列是點標識,第二列儲存該點起始的所有的邊集,內容如:2:2,3:1,4:4。<br />
* 第 106 行:主程式(main 函式),定義 GraphJob,指定 Vertex/GraphLoader/Combiner 等的實現,指定輸入輸出表。
<a name="PageRank"></a>
## PageRank
PageRank 演算法是計算網頁排名的經典演算法:輸入是一個有向圖 G,其中頂點表示網頁,如果存在網頁 A 到網頁 B 的連結,那麼存在連線 A 到 B 的邊。<br />演算法基本原理,如下所示:
* 初始化:點值表示 PageRank 的 rank 值(double 型別),初始時,所有點取值為 1/TotalNumVertices。<br />
* 迭代公式:PageRank(i)=0.15/TotalNumVertices+0.85*sum,其中 sum 為所有指向 i 點的點(設為 j) PageRank(j)/out_degree(j) 的累加值。<br />

由演算法基本原理可以看出,此演算法非常適合使用 MaxCompute Graph 程式進行求解:每個點 j 維護其 PageRank 值,每一輪迭代都將 PageRank(j)/out_degree(j) 發給其鄰接點(向其投票),下一輪迭代時,每個點根據迭代公式重新計算 PageRank 取值。
<a name="b5ea48ff"></a>
### []()程式碼示例複製程式碼

import java.io.IOException;
import org.apache.log4j.Logger;
import com.aliyun.odps.io.WritableRecord;
import com.aliyun.odps.graph.ComputeContext;
import com.aliyun.odps.graph.GraphJob;
import com.aliyun.odps.graph.GraphLoader;
import com.aliyun.odps.graph.MutationContext;
import com.aliyun.odps.graph.Vertex;
import com.aliyun.odps.graph.WorkerContext;
import com.aliyun.odps.io.DoubleWritable;
import com.aliyun.odps.io.LongWritable;
import com.aliyun.odps.io.NullWritable;
import com.aliyun.odps.data.TableInfo;
import com.aliyun.odps.io.Text;
import com.aliyun.odps.io.Writable;
public class PageRank {
private final static Logger LOG = Logger.getLogger(PageRank.class);
public static class PageRankVertex extends

Vertex<Text, DoubleWritable, NullWritable, DoubleWritable> {
@Override
public void compute(
    ComputeContext<Text, DoubleWritable, NullWritable, DoubleWritable> context,
    Iterable<DoubleWritable> messages) throws IOException {
  if (context.getSuperstep() == 0) {
    setValue(new DoubleWritable(1.0 / context.getTotalNumVertices()));
  } else if (context.getSuperstep() >= 1) {
    double sum = 0;
    for (DoubleWritable msg : messages) {
      sum += msg.get();
    }
    DoubleWritable vertexValue = new DoubleWritable(
        (0.15f / context.getTotalNumVertices()) + 0.85f * sum);
    setValue(vertexValue);
  }
  if (hasEdges()) {
    context.sendMessageToNeighbors(this, new DoubleWritable(getValue()
        .get() / getEdges().size()));
  }
}
@Override
public void cleanup(
    WorkerContext<Text, DoubleWritable, NullWritable, DoubleWritable> context)
    throws IOException {
  context.write(getId(), getValue());
}複製程式碼

}
public static class PageRankVertexReader extends

GraphLoader<Text, DoubleWritable, NullWritable, DoubleWritable> {
@Override
public void load(
    LongWritable recordNum,
    WritableRecord record,
    MutationContext<Text, DoubleWritable, NullWritable, DoubleWritable> context)
    throws IOException {
  PageRankVertex vertex = new PageRankVertex();
  vertex.setValue(new DoubleWritable(0));
  vertex.setId((Text) record.get(0));
  System.out.println(record.get(0));
  for (int i = 1; i < record.size(); i++) {
    Writable edge = record.get(i);
    System.out.println(edge.toString());
    if (!(edge.equals(NullWritable.get()))) {
      vertex.addEdge(new Text(edge.toString()), NullWritable.get());
    }
  }
  LOG.info("vertex edgs size: "
      + (vertex.hasEdges() ? vertex.getEdges().size() : 0));
  context.addVertexRequest(vertex);
}複製程式碼

}
private static void printUsage() {

System.out.println("Usage: <in> <out> [Max iterations (default 30)]");
System.exit(-1);複製程式碼

}
public static void main(String[] args) throws IOException {

if (args.length < 2)
  printUsage();
GraphJob job = new GraphJob();
job.setGraphLoaderClass(PageRankVertexReader.class);
job.setVertexClass(PageRankVertex.class);
job.addInput(TableInfo.builder().tableName(args[0]).build());
job.addOutput(TableInfo.builder().tableName(args[1]).build());
// default max iteration is 30
job.setMaxIteration(30);
if (args.length >= 3)
  job.setMaxIteration(Integer.parseInt(args[2]));
long startTime = System.currentTimeMillis();
job.run();
System.out.println("Job Finished in "
    + (System.currentTimeMillis() - startTime) / 1000.0 + " seconds");複製程式碼

}
}

上述程式碼,說明如下:
* 第 23 行:定義 PageRankVertex ,其中:
  * 點值表示該點(網頁)的當前 PageRank 取值。<br />
  * compute() 方法使用迭代公式:`PageRank(i)=0.15/TotalNumVertices+0.85*sum`更新點值。<br />
  * cleanup() 方法把點及其 PageRank 取值寫到結果表中。<br />
* 第 55 行:定義 PageRankVertexReader 類,載入圖,將表中每一條記錄解析為一個點,記錄的第一列是起點,其他列為終點。<br />
* 第 88 行:主程式(main 函式),定義 GraphJob,指定 Vertex/GraphLoader 等的實現,以及最大迭代次數(預設 30),並指定輸入輸出表。
<a name="5732cb27"></a>
## K-均值聚類
k-均值聚類(Kmeans) 演算法是非常基礎並大量使用的聚類演算法。<br />演算法基本原理:以空間中 k 個點為中心進行聚類,對最靠近它們的點進行歸類。通過迭代的方法,逐次更新各聚類中心的值,直至得到最好的聚類結果。<br />假設要把樣本集分為 k 個類別,演算法描述如下:
1. 適當選擇 k 個類的初始中心。<br />
1. 在第 i 次迭代中,對任意一個樣本,求其到 k 箇中心的距離,將該樣本歸到距離最短的中心所在的類。<br />
1. 利用均值等方法更新該類的中心值。<br />
1. 對於所有的 k 個聚類中心,如果利用上兩步的迭代法更新後,值保持不變或者小於某個閾值,則迭代結束,否則繼續迭代。<br />
<a name="b5ea48ff"></a>
### []()程式碼示例
K-均值聚類演算法的程式碼,如下所示:複製程式碼

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.log4j.Logger;
import com.aliyun.odps.io.WritableRecord;
import com.aliyun.odps.graph.Aggregator;
import com.aliyun.odps.graph.ComputeContext;
import com.aliyun.odps.graph.GraphJob;
import com.aliyun.odps.graph.GraphLoader;
import com.aliyun.odps.graph.MutationContext;
import com.aliyun.odps.graph.Vertex;
import com.aliyun.odps.graph.WorkerContext;
import com.aliyun.odps.io.DoubleWritable;
import com.aliyun.odps.io.LongWritable;
import com.aliyun.odps.io.NullWritable;
import com.aliyun.odps.data.TableInfo;
import com.aliyun.odps.io.Text;
import com.aliyun.odps.io.Tuple;
import com.aliyun.odps.io.Writable;
public class Kmeans {
private final static Logger LOG = Logger.getLogger(Kmeans.class);
public static class KmeansVertex extends

Vertex<Text, Tuple, NullWritable, NullWritable> {
@Override
public void compute(
    ComputeContext<Text, Tuple, NullWritable, NullWritable> context,
    Iterable<NullWritable> messages) throws IOException {
  context.aggregate(getValue());
}複製程式碼

}
public static class KmeansVertexReader extends

GraphLoader<Text, Tuple, NullWritable, NullWritable> {
@Override
public void load(LongWritable recordNum, WritableRecord record,
    MutationContext<Text, Tuple, NullWritable, NullWritable> context)
    throws IOException {
  KmeansVertex vertex = new KmeansVertex();
  vertex.setId(new Text(String.valueOf(recordNum.get())));
  vertex.setValue(new Tuple(record.getAll()));
  context.addVertexRequest(vertex);
}複製程式碼

}
public static class KmeansAggrValue implements Writable {

Tuple centers = new Tuple();
Tuple sums = new Tuple();
Tuple counts = new Tuple();
@Override
public void write(DataOutput out) throws IOException {
  centers.write(out);
  sums.write(out);
  counts.write(out);
}
@Override
public void readFields(DataInput in) throws IOException {
  centers = new Tuple();
  centers.readFields(in);
  sums = new Tuple();
  sums.readFields(in);
  counts = new Tuple();
  counts.readFields(in);
}
@Override
public String toString() {
  return "centers " + centers.toString() + ", sums " + sums.toString()
      + ", counts " + counts.toString();
}複製程式碼

}
public static class KmeansAggregator extends Aggregator {

@SuppressWarnings("rawtypes")
@Override
public KmeansAggrValue createInitialValue(WorkerContext context)
    throws IOException {
  KmeansAggrValue aggrVal = null;
  if (context.getSuperstep() == 0) {
    aggrVal = new KmeansAggrValue();
    aggrVal.centers = new Tuple();
    aggrVal.sums = new Tuple();
    aggrVal.counts = new Tuple();
    byte[] centers = context.readCacheFile("centers");
    String lines[] = new String(centers).split("\n");
    for (int i = 0; i < lines.length; i++) {
      String[] ss = lines[i].split(",");
      Tuple center = new Tuple();
      Tuple sum = new Tuple();
      for (int j = 0; j < ss.length; ++j) {
        center.append(new DoubleWritable(Double.valueOf(ss[j].trim())));
        sum.append(new DoubleWritable(0.0));
      }
      LongWritable count = new LongWritable(0);
      aggrVal.sums.append(sum);
      aggrVal.counts.append(count);
      aggrVal.centers.append(center);
    }
  } else {
    aggrVal = (KmeansAggrValue) context.getLastAggregatedValue(0);
  }
  return aggrVal;
}
@Override
public void aggregate(KmeansAggrValue value, Object item) {
  int min = 0;
  double mindist = Double.MAX_VALUE;
  Tuple point = (Tuple) item;
  for (int i = 0; i < value.centers.size(); i++) {
    Tuple center = (Tuple) value.centers.get(i);
    // use Euclidean Distance, no need to calculate sqrt
    double dist = 0.0d;
    for (int j = 0; j < center.size(); j++) {
      double v = ((DoubleWritable) point.get(j)).get()
          - ((DoubleWritable) center.get(j)).get();
      dist += v * v;
    }
    if (dist < mindist) {
      mindist = dist;
      min = i;
    }
  }
  // update sum and count
  Tuple sum = (Tuple) value.sums.get(min);
  for (int i = 0; i < point.size(); i++) {
    DoubleWritable s = (DoubleWritable) sum.get(i);
    s.set(s.get() + ((DoubleWritable) point.get(i)).get());
  }
  LongWritable count = (LongWritable) value.counts.get(min);
  count.set(count.get() + 1);
}
@Override
public void merge(KmeansAggrValue value, KmeansAggrValue partial) {
  for (int i = 0; i < value.sums.size(); i++) {
    Tuple sum = (Tuple) value.sums.get(i);
    Tuple that = (Tuple) partial.sums.get(i);
    for (int j = 0; j < sum.size(); j++) {
      DoubleWritable s = (DoubleWritable) sum.get(j);
      s.set(s.get() + ((DoubleWritable) that.get(j)).get());
    }
  }
  for (int i = 0; i < value.counts.size(); i++) {
    LongWritable count = (LongWritable) value.counts.get(i);
    count.set(count.get() + ((LongWritable) partial.counts.get(i)).get());
  }
}
@SuppressWarnings("rawtypes")
@Override
public boolean terminate(WorkerContext context, KmeansAggrValue value)
    throws IOException {
  // compute new centers
  Tuple newCenters = new Tuple(value.sums.size());
  for (int i = 0; i < value.sums.size(); i++) {
    Tuple sum = (Tuple) value.sums.get(i);
    Tuple newCenter = new Tuple(sum.size());
    LongWritable c = (LongWritable) value.counts.get(i);
    for (int j = 0; j < sum.size(); j++) {
      DoubleWritable s = (DoubleWritable) sum.get(j);
      double val = s.get() / c.get();
      newCenter.set(j, new DoubleWritable(val));
      // reset sum for next iteration
      s.set(0.0d);
    }
    // reset count for next iteration
    c.set(0);
    newCenters.set(i, newCenter);
  }
  // update centers
  Tuple oldCenters = value.centers;
  value.centers = newCenters;
  LOG.info("old centers: " + oldCenters + ", new centers: " + newCenters);
  // compare new/old centers
  boolean converged = true;
  for (int i = 0; i < value.centers.size() && converged; i++) {
    Tuple oldCenter = (Tuple) oldCenters.get(i);
    Tuple newCenter = (Tuple) newCenters.get(i);
    double sum = 0.0d;
    for (int j = 0; j < newCenter.size(); j++) {
      double v = ((DoubleWritable) newCenter.get(j)).get()
          - ((DoubleWritable) oldCenter.get(j)).get();
      sum += v * v;
    }
    double dist = Math.sqrt(sum);
    LOG.info("old center: " + oldCenter + ", new center: " + newCenter
        + ", dist: " + dist);
    // converge threshold for each center: 0.05
    converged = dist < 0.05d;
  }
  if (converged || context.getSuperstep() == context.getMaxIteration() - 1) {
    // converged or reach max iteration, output centers
    for (int i = 0; i < value.centers.size(); i++) {
      context.write(((Tuple) value.centers.get(i)).toArray());
    }
    // true means to terminate iteration
    return true;
  }
  // false means to continue iteration
  return false;
}複製程式碼

}
private static void printUsage() {

System.out.println("Usage: <in> <out> [Max iterations (default 30)]");
System.exit(-1);複製程式碼

}
public static void main(String[] args) throws IOException {

if (args.length < 2)
  printUsage();
GraphJob job = new GraphJob();
job.setGraphLoaderClass(KmeansVertexReader.class);
job.setRuntimePartitioning(false);
job.setVertexClass(KmeansVertex.class);
job.setAggregatorClass(KmeansAggregator.class);
job.addInput(TableInfo.builder().tableName(args[0]).build());
job.addOutput(TableInfo.builder().tableName(args[1]).build());
// default max iteration is 30
job.setMaxIteration(30);
if (args.length >= 3)
  job.setMaxIteration(Integer.parseInt(args[2]));
long start = System.currentTimeMillis();
job.run();
System.out.println("Job Finished in "
    + (System.currentTimeMillis() - start) / 1000.0 + " seconds");複製程式碼

}
}

<br />上述程式碼,說明如下:
* 第 26 行:定義 KmeansVertex,compute() 方法非常簡單,只是呼叫上下文物件的 aggregate 方法,傳入當前點的取值(Tuple 型別,向量表示)。<br />
* 第 38 行:定義 KmeansVertexReader 類,載入圖,將表中每一條記錄解析為一個點,點標識無關緊要,這裡取傳入的 recordNum 序號作為標識,點值為記錄的所有列組成的 Tuple。<br />
* 第 83 行:定義 KmeansAggregator,這個類封裝了 Kmeans 演算法的主要邏輯,其中:
  * createInitialValue 為每一輪迭代建立初始值(k 類中心點),若是第一輪迭代(superstep=0),該取值為初始中心點,否則取值為上一輪結束時的新中心點。<br />
  * aggregate 方法為每個點計算其到各個類中心的距離,並歸為距離最短的類,並更新該類的 sum 和 count。<br />
  * merge 方法合併來自各個 worker 收集的 sum 和 count。<br />
  * terminate 方法根據各個類的 sum 和 count 計算新的中心點,若新中心點與之前的中心點距離小於某個閾值或者迭代次數到達最大迭代次數設定,則終止迭代(返回 false),寫最終的中心點到結果表。<br />
* 第 236 行:主程式(main 函式),定義 GraphJob,指定 Vertex/GraphLoader/Aggregator 等的實現,以及最大迭代次數(預設 30),並指定輸入輸出表。<br />
* 第 243 行:job.setRuntimePartitioning(false),對於 Kmeans 演算法,載入圖是不需要進行點的分發,設定 RuntimePartitioning 為 false,以提升載入圖時的效能。
<a name="BiPartiteMatchiing"></a>
## BiPartiteMatchiing
二分圖是指圖的所有頂點可分為兩個集合,每條邊對應的兩個頂點分別屬於這兩個集合。對於一個二分圖 G,M 是它的一個子圖,如果 M 的邊集中任意兩條邊都不依附於同一個頂點,則稱 M 為一個匹配。二分圖匹配常用於有明確供需關係場景(如交友網站等)下的資訊匹配行為。<br />演算法描述,如下所示:
* 從左邊第 1 個頂點開始,挑選未匹配點進行搜尋,尋找增廣路。<br />
* 如果經過一個未匹配點,說明尋找成功。<br />
* 更新路徑資訊,匹配邊數 +1,停止搜尋。<br />
* 如果一直沒有找到增廣路,則不再從這個點開始搜尋。<br />
<a name="b5ea48ff"></a>
### []()程式碼示例
BiPartiteMatchiing 演算法的程式碼,如下所示:複製程式碼

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Random;
import com.aliyun.odps.data.TableInfo;
import com.aliyun.odps.graph.ComputeContext;
import com.aliyun.odps.graph.GraphJob;
import com.aliyun.odps.graph.MutationContext;
import com.aliyun.odps.graph.WorkerContext;
import com.aliyun.odps.graph.Vertex;
import com.aliyun.odps.graph.GraphLoader;
import com.aliyun.odps.io.LongWritable;
import com.aliyun.odps.io.NullWritable;
import com.aliyun.odps.io.Text;
import com.aliyun.odps.io.Writable;
import com.aliyun.odps.io.WritableRecord;
public class BipartiteMatching {
private static final Text UNMATCHED = new Text("UNMATCHED");
public static class TextPair implements Writable {

public Text first;
public Text second;
public TextPair() {
  first = new Text();
  second = new Text();
}
public TextPair(Text first, Text second) {
  this.first = new Text(first);
  this.second = new Text(second);
}
@Override
public void write(DataOutput out) throws IOException {
  first.write(out);
  second.write(out);
}
@Override
public void readFields(DataInput in) throws IOException {
  first = new Text();
  first.readFields(in);
  second = new Text();
  second.readFields(in);
}
@Override
public String toString() {
  return first + ": " + second;
}複製程式碼

}
public static class BipartiteMatchingVertexReader extends

GraphLoader<Text, TextPair, NullWritable, Text> {
@Override
public void load(LongWritable recordNum, WritableRecord record,
    MutationContext<Text, TextPair, NullWritable, Text> context)
    throws IOException {
  BipartiteMatchingVertex vertex = new BipartiteMatchingVertex();
  vertex.setId((Text) record.get(0));
  vertex.setValue(new TextPair(UNMATCHED, (Text) record.get(1)));
  String[] adjs = record.get(2).toString().split(",");
  for (String adj : adjs) {
    vertex.addEdge(new Text(adj), null);
  }
  context.addVertexRequest(vertex);
}複製程式碼

}
public static class BipartiteMatchingVertex extends

Vertex<Text, TextPair, NullWritable, Text> {
private static final Text LEFT = new Text("LEFT");
private static final Text RIGHT = new Text("RIGHT");
private static Random rand = new Random();
@Override
public void compute(
    ComputeContext<Text, TextPair, NullWritable, Text> context,
    Iterable<Text> messages) throws IOException {
  if (isMatched()) {
    voteToHalt();
    return;
  }
  switch ((int) context.getSuperstep() % 4) {
  case 0:
    if (isLeft()) {
      context.sendMessageToNeighbors(this, getId());
    }
    break;
  case 1:
    if (isRight()) {
      Text luckyLeft = null;
      for (Text message : messages) {
        if (luckyLeft == null) {
          luckyLeft = new Text(message);
        } else {
          if (rand.nextInt(1) == 0) {
            luckyLeft.set(message);
          }
        }
      }
      if (luckyLeft != null) {
        context.sendMessage(luckyLeft, getId());
      }
    }
    break;
  case 2:
    if (isLeft()) {
      Text luckyRight = null;
      for (Text msg : messages) {
        if (luckyRight == null) {
          luckyRight = new Text(msg);
        } else {
          if (rand.nextInt(1) == 0) {
            luckyRight.set(msg);
          }
        }
      }
      if (luckyRight != null) {
        setMatchVertex(luckyRight);
        context.sendMessage(luckyRight, getId());
      }
    }
    break;
  case 3:
    if (isRight()) {
      for (Text msg : messages) {
        setMatchVertex(msg);
      }
    }
    break;
  }
}
@Override
public void cleanup(
    WorkerContext<Text, TextPair, NullWritable, Text> context)
    throws IOException {
  context.write(getId(), getValue().first);
}
private boolean isMatched() {
  return !getValue().first.equals(UNMATCHED);
}
private boolean isLeft() {
  return getValue().second.equals(LEFT);
}
private boolean isRight() {
  return getValue().second.equals(RIGHT);
}
private void setMatchVertex(Text matchVertex) {
  getValue().first.set(matchVertex);
}複製程式碼

}
private static void printUsage() {

System.err.println("BipartiteMatching <input> <output> [maxIteration]");複製程式碼

}
public static void main(String[] args) throws IOException {

if (args.length < 2) {
  printUsage();
}
GraphJob job = new GraphJob();
job.setGraphLoaderClass(BipartiteMatchingVertexReader.class);
job.setVertexClass(BipartiteMatchingVertex.class);
job.addInput(TableInfo.builder().tableName(args[0]).build());
job.addOutput(TableInfo.builder().tableName(args[1]).build());
int maxIteration = 30;
if (args.length > 2) {
  maxIteration = Integer.parseInt(args[2]);
}
job.setMaxIteration(maxIteration);
job.run();複製程式碼

}
}

<a name="d41d8cd9"></a>
## 複製程式碼


原文連結

本文為雲棲社群原創內容,未經允許不得轉載。


相關文章