DL4J之CNN对今日头条文本分类

一、数据集介绍

    数据来源:今日头条客户端

    数据格式如下:

16551700932705387022_!_101_!_news_culture_!_京城最值得你来场文化之旅的博物馆_!_保利集团,马未都,中国科学技术馆,博物馆,新中国 26552368441838272771_!_101_!_news_culture_!_发酵床的垫料种类有哪些?哪种更好?_!_ 36552407965343678723_!_101_!_news_culture_!_上联:黄山黄河黄皮肤黄土高原。怎么对下联?_!_ 46552332417753940238_!_101_!_news_culture_!_林徽因什么理由拒绝了徐志摩而选择梁思成为终身伴侣?_!_ 56552475601595269390_!_101_!_news_culture_!_黄杨木是什么树?_!_

    每行为一条数据,以_!_分割的个字段,从前往后分别是 新闻ID,分类code(见下文),分类名称(见下文),新闻字符串(仅含标题),新闻关键词

    分类code与名称:

1100 民生 故事 news_story 2101 文化 文化 news_culture 3102 娱乐 娱乐 news_entertainment 4103 体育 体育 news_sports 5104 财经 财经 news_finance 6106 房产 房产 news_house 7107 汽车 汽车 news_car 8108 教育 教育 news_edu 9109 科技 科技 news_tech 10110 军事 军事 news_military 11112 旅游 旅游 news_travel 12113 国际 国际 news_world 13114 证券 股票 stock 14115 农业 三农 news_agriculture 15116 电竞 游戏 news_game

    github地址:https://github.com/fate233/toutiao-text-classfication-dataset

    数据资源中给出了分类的实验结果:

1Test Loss: 0.57, Test Acc: 83.81% 2 3 precision recall f1-score support 4 5 news_story 0.66 0.75 0.70 848 6 7 news_culture 0.57 0.83 0.68 1531 8 9news_entertainment 0.86 0.86 0.86 8078 10 11 news_sports 0.94 0.91 0.92 7338 12 13 news_finance 0.59 0.67 0.63 1594 14 15 news_house 0.84 0.89 0.87 1478 16 17 news_car 0.92 0.90 0.91 6481 18 19 news_edu 0.71 0.86 0.77 1425 20 21 news_tech 0.85 0.84 0.85 6944 22 23 news_military 0.90 0.78 0.84 6174 24 25 news_travel 0.58 0.76 0.66 1287 26 27 news_world 0.72 0.69 0.70 3823 28 29 stock 0.00 0.00 0.00 53 30 31 news_agriculture 0.80 0.88 0.84 1701 32 33 news_game 0.92 0.87 0.89 6244 34 35 avg / total 0.85 0.84 0.84 54999

   下面我们就来用deeplearning4j来实现一个卷积结构对该数据集进行分类,看能不能得到更好的结果。

二、卷积网络可以用于文本处理的原因

    CNN非常适合处理图像数据,前面一篇文章《deeplearning4j——卷积神经网络对验证码进行识别》介绍了CNN对验证码进行识别。本篇博客将利用CNN对文本进行分类,在开始之前我们先来直观的说说卷积运算在做的本质事情是什么。卷积运算,本质上可以看做两个向量的点积,两个向量越同向,点积就越大,经过relu和MaxPooling之后,本质上是提取了与卷积核最同向的结构,这个“结构”实际上是图片上的一些线条。

    那么文本可以用CNN来处理吗?答案是肯定的,文本每个词用向量表示之后,依次排开,就变成了一张二维图,如下图,沿着红色箭头的方向(也就是文本的方向)看,两个句子用一幅图表示之后,会出现相同的单元,也就可以用CNN来处理。

    

三、文本处理的卷积结构

    那么,怎么设计这个CNN网络结构呢?如下图:(论文地址:https://arxiv.org/abs/1408.5882

    

   注意点:

   1、卷积核移动的方向必须为句子的方向

   2、每个卷积核提取的特征为N行1列的向量

   3、MaxPooling的操作的对象是每一个Feature Map,也就是从每一个N行1列的向量中选择一个最大值

   4、把选择的所有最大值接起来,经过几个Fully Connected 层,进行分类

四、数据的预处理与词向量

    1、分词工具:HanLP

    2、处理后的数据格式如下:(类别code_!_词,其中,词与词之间用空格隔开,_!_为分割符)

   

    数据预处理代码如下:

1public static void main(String[] args) throws Exception { 2 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( 3 new FileInputStream(new File("/toutiao_cat_data/toutiao_cat_data.txt")), "UTF-8")); 4 OutputStreamWriter writerStream = new OutputStreamWriter( 5 new FileOutputStream("/toutiao_cat_data/toutiao_data_type_word.txt"), "UTF-8"); 6 BufferedWriter writer = new BufferedWriter(writerStream); 7 String line = null; 8 long startTime = System.currentTimeMillis(); 9 while ((line = bufferedReader.readLine()) != null) { 10 String[] array = line.split("_!_"); 11 StringBuilder stringBuilder = new StringBuilder(); 12 for (Term term : HanLP.segment(array[3])) { 13 if (stringBuilder.length() > 0) { 14 stringBuilder.append(" "); 15 } 16 stringBuilder.append(term.word.trim()); 17 } 18 writer.write(Integer.parseInt(array[1].trim()) + "_!_" + stringBuilder.toString() + "\n"); 19 } 20 writer.flush(); 21 writer.close(); 22 System.out.println(System.currentTimeMillis() - startTime); 23 bufferedReader.close(); 24 }

五、词的向量表示

    1、one-hot

    用正交的向量来表示每一个词,这样表示无法反应词与词之间的关系,那么两句话中,要想复用同一个卷积核,那么必须出现一模一样的词才可以,实际上,我们要求模型可以举一反三,连相似的结构也可以提取,那么word2vec可以解决这个问题。

    2、word2vec

    word2vec可以充分考虑词与词之间的关系,相似的词,肯定有某些维度靠的比较近。那么也就考虑了词的语句之间的关系,训练word2vec有两种,skipgram和cbow,下面我们用cbow来训练词向量,结果会持久化下来,就得到了toutiao.vec的文件,下次变可重新加载该文件获得词的向量表示,代码如下:

1String filePath = new ClassPathResource("toutiao_data_word.txt").getFile().getAbsolutePath(); 2 SentenceIterator iter = new BasicLineIterator(filePath); 3 TokenizerFactory t = new DefaultTokenizerFactory(); 4 t.setTokenPreProcessor(new CommonPreprocessor()); 5 VocabCache<VocabWord> cache = new AbstractCache<>(); 6 WeightLookupTable<VocabWord> table = new InMemoryLookupTable.Builder<VocabWord>().vectorLength(100) 7 .useAdaGrad(false).cache(cache).build(); 8 9 log.info("Building model...."); 10 Word2Vec vec = new Word2Vec.Builder() 11 .elementsLearningAlgorithm("org.deeplearning4j.models.embeddings.learning.impl.elements.CBOW") 12 .minWordFrequency(0).iterations(1).epochs(20).layerSize(100).seed(42).windowSize(8).iterate(iter) 13 .tokenizerFactory(t).lookupTable(table).vocabCache(cache).build(); 14 15 vec.fit(); 16 WordVectorSerializer.writeWord2VecModel(vec, "/toutiao_cat_data/toutiao.vec");

六、CNN网络结构

    CNN网络结构如下:

    说明:

    1、cnn3、cnn4、cnn5、cnn6卷积核大小为(3,vectorSize)、(4,vectorSize)、(5,vectorSize)、(6,vectorSize),步幅为1,也就是分别读取3、4、5、6个词,提取特征

    2、cnn3-stride2、cnn4-stride2、cnn5-stride2、cnn6-stride2卷积核大小为(3,vectorSize)、(4,vectorSize)、(5,vectorSize)、(6,vectorSize),步幅为2

    3、两组卷积核卷积的结果合并,分别得到merge1和merge2,都是4维张量,形状分别为(batchSize,depth1+depth2+depth3,height/1,1),(batchSize,depth1+depth2+depth3,height/2,1),特别说明:这里的卷积模式为ConvolutionMode.Same

    4、merge1、2分别经过MaxPooling,这里用的是GlobalPoolingLayer,和平台的Pooling层不同,这里会从指定维度中,取一个最大值,所以经过GlobalPoolingLayer之后,merge1、2分别变成2维张量,形状为(batchSize,depth1+depth2+depth3),那么GlobalPoolingLayer是如何求Max的呢?源码如下:

1private INDArray activateHelperFullArray(INDArray inputArray, int[] poolDim) { 2 switch (poolingType) { 3 case MAX: 4 return inputArray.max(poolDim); 5 case AVG: 6 return inputArray.mean(poolDim); 7 case SUM: 8 return inputArray.sum(poolDim); 9 case PNORM: 10 //P norm: https://arxiv.org/pdf/1311.1780.pdf 11 //out = (1/N * sum( |in| ^ p) ) ^ (1/p) 12 int pnorm = layerConf().getPnorm(); 13 14 INDArray abs = Transforms.abs(inputArray, true); 15 Transforms.pow(abs, pnorm, false); 16 INDArray pNorm = abs.sum(poolDim); 17 18 return Transforms.pow(pNorm, 1.0 / pnorm, false); 19 default: 20 throw new RuntimeException("Unknown or not supported pooling type: " + poolingType + " " + layerId()); 21 } 22 }

    5、两边GlobalPoolingLayer结果再接起来,丢给全连接网络,经过softmax分类器进行分类

    6、fc层,用了0.5的dropout防止过拟合,在下面的代码中可以看到。

完整代码如下:

1public class CnnSentenceClassificationTouTiao { 2 3 public static void main(String[] args) throws Exception { 4 5 List<String> trainLabelList = new ArrayList<>();// 训练集label 6 List<String> trainSentences = new ArrayList<>();// 训练集文本集合 7 List<String> testLabelList = new ArrayList<>();// 测试集label 8 List<String> testSentences = new ArrayList<>();//// 测试集文本集合 9 Map<String, List<String>> map = new HashMap<>(); 10 11 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( 12 new FileInputStream(new File("/toutiao_cat_data/toutiao_data_type_word.txt")), "UTF-8")); 13 String line = null; 14 int truncateReviewsToLength = 0; 15 Random random = new Random(123); 16 while ((line = bufferedReader.readLine()) != null) { 17 String[] array = line.split("_!_"); 18 if (map.get(array[0]) == null) { 19 map.put(array[0], new ArrayList<String>()); 20 } 21 map.get(array[0]).add(array[1]);// 将样本中所有数据,按照类别归类 22 int length = array[1].split(" ").length; 23 if (length > truncateReviewsToLength) { 24 truncateReviewsToLength = length;// 求样本中,句子的最大长度 25 } 26 } 27 bufferedReader.close(); 28 for (Map.Entry<String, List<String>> entry : map.entrySet()) { 29 for (String sentence : entry.getValue()) { 30 if (random.nextInt() % 5 == 0) {// 每个类别抽取20%作为test集 31 testLabelList.add(entry.getKey()); 32 testSentences.add(sentence); 33 } else { 34 trainLabelList.add(entry.getKey()); 35 trainSentences.add(sentence); 36 } 37 } 38 39 } 40 int batchSize = 64; 41 int vectorSize = 100; 42 int nEpochs = 10; 43 44 int cnnLayerFeatureMaps = 50; 45 PoolingType globalPoolingType = PoolingType.MAX; 46 Random rng = new Random(12345); 47 Nd4j.getMemoryManager().setAutoGcWindow(5000); 48 49 ComputationGraphConfiguration config = new NeuralNetConfiguration.Builder().weightInit(WeightInit.RELU) 50 .activation(Activation.LEAKYRELU).updater(new Nesterovs(0.01, 0.9)) 51 .convolutionMode(ConvolutionMode.Same).l2(0.0001).graphBuilder().addInputs("input") 52 .addLayer("cnn3", 53 new ConvolutionLayer.Builder().kernelSize(3, vectorSize).stride(1, vectorSize) 54 .nOut(cnnLayerFeatureMaps).build(), 55 "input") 56 .addLayer("cnn4", 57 new ConvolutionLayer.Builder().kernelSize(4, vectorSize).stride(1, vectorSize) 58 .nOut(cnnLayerFeatureMaps).build(), 59 "input") 60 .addLayer("cnn5", 61 new ConvolutionLayer.Builder().kernelSize(5, vectorSize).stride(1, vectorSize) 62 .nOut(cnnLayerFeatureMaps).build(), 63 "input") 64 .addLayer("cnn6", 65 new ConvolutionLayer.Builder().kernelSize(6, vectorSize).stride(1, vectorSize) 66 .nOut(cnnLayerFeatureMaps).build(), 67 "input") 68 .addLayer("cnn3-stride2", 69 new ConvolutionLayer.Builder().kernelSize(3, vectorSize).stride(2, vectorSize) 70 .nOut(cnnLayerFeatureMaps).build(), 71 "input") 72 .addLayer("cnn4-stride2", 73 new ConvolutionLayer.Builder().kernelSize(4, vectorSize).stride(2, vectorSize) 74 .nOut(cnnLayerFeatureMaps).build(), 75 "input") 76 .addLayer("cnn5-stride2", 77 new ConvolutionLayer.Builder().kernelSize(5, vectorSize).stride(2, vectorSize) 78 .nOut(cnnLayerFeatureMaps).build(), 79 "input") 80 .addLayer("cnn6-stride2", 81 new ConvolutionLayer.Builder().kernelSize(6, vectorSize).stride(2, vectorSize) 82 .nOut(cnnLayerFeatureMaps).build(), 83 "input") 84 .addVertex("merge1", new MergeVertex(), "cnn3", "cnn4", "cnn5", "cnn6") 85 .addLayer("globalPool1", new GlobalPoolingLayer.Builder().poolingType(globalPoolingType).build(), 86 "merge1") 87 .addVertex("merge2", new MergeVertex(), "cnn3-stride2", "cnn4-stride2", "cnn5-stride2", "cnn6-stride2") 88 .addLayer("globalPool2", new GlobalPoolingLayer.Builder().poolingType(globalPoolingType).build(), 89 "merge2") 90 .addLayer("fc", 91 new DenseLayer.Builder().nOut(200).dropOut(0.5).activation(Activation.LEAKYRELU).build(), 92 "globalPool1", "globalPool2") 93 .addLayer("out", 94 new OutputLayer.Builder().lossFunction(LossFunctions.LossFunction.MCXENT) 95 .activation(Activation.SOFTMAX).nOut(15).build(), 96 "fc") 97 .setOutputs("out").setInputTypes(InputType.convolutional(truncateReviewsToLength, vectorSize, 1)) 98 .build(); 99 100 ComputationGraph net = new ComputationGraph(config); 101 net.init(); 102 System.out.println(net.summary()); 103 Word2Vec word2Vec = WordVectorSerializer.readWord2VecModel("/toutiao_cat_data/toutiao.vec"); 104 System.out.println("Loading word vectors and creating DataSetIterators"); 105 DataSetIterator trainIter = getDataSetIterator(word2Vec, batchSize, truncateReviewsToLength, trainLabelList, 106 trainSentences, rng); 107 DataSetIterator testIter = getDataSetIterator(word2Vec, batchSize, truncateReviewsToLength, testLabelList, 108 testSentences, rng); 109 110 UIServer uiServer = UIServer.getInstance(); 111 StatsStorage statsStorage = new InMemoryStatsStorage(); 112 uiServer.attach(statsStorage); 113 net.setListeners(new ScoreIterationListener(100), new StatsListener(statsStorage, 20), 114 new EvaluativeListener(testIter, 1, InvocationType.EPOCH_END)); 115 116 // net.setListeners(new ScoreIterationListener(100), 117 // new EvaluativeListener(testIter, 1, InvocationType.EPOCH_END)); 118 net.fit(trainIter, nEpochs); 119 } 120 121 private static DataSetIterator getDataSetIterator(WordVectors wordVectors, int minibatchSize, int maxSentenceLength, 122 List<String> lableList, List<String> sentences, Random rng) { 123 124 LabeledSentenceProvider sentenceProvider = new CollectionLabeledSentenceProvider(sentences, lableList, rng); 125 126 return new CnnSentenceDataSetIterator.Builder().sentenceProvider(sentenceProvider).wordVectors(wordVectors) 127 .minibatchSize(minibatchSize).maxSentenceLength(maxSentenceLength).useNormalizedWordVectors(false) 128 .build(); 129 } 130}

 代码说明:

    1、代码分两部分,第一部分是数据预处理,分出20%测试集、80%作为训练集

    2、第二部分为网络的基本结构代码

网络参数详细如下:

1=============================================================================================================================================== 2VertexName (VertexType) nIn,nOut TotalParams ParamsShape Vertex Inputs 3=============================================================================================================================================== 4input (InputVertex) -,- - - - 5cnn3 (ConvolutionLayer) 1,50 15050 W:{50,1,3,100}, b:{1,50} [input] 6cnn4 (ConvolutionLayer) 1,50 20050 W:{50,1,4,100}, b:{1,50} [input] 7cnn5 (ConvolutionLayer) 1,50 25050 W:{50,1,5,100}, b:{1,50} [input] 8cnn6 (ConvolutionLayer) 1,50 30050 W:{50,1,6,100}, b:{1,50} [input] 9cnn3-stride2 (ConvolutionLayer) 1,50 15050 W:{50,1,3,100}, b:{1,50} [input] 10cnn4-stride2 (ConvolutionLayer) 1,50 20050 W:{50,1,4,100}, b:{1,50} [input] 11cnn5-stride2 (ConvolutionLayer) 1,50 25050 W:{50,1,5,100}, b:{1,50} [input] 12cnn6-stride2 (ConvolutionLayer) 1,50 30050 W:{50,1,6,100}, b:{1,50} [input] 13merge1 (MergeVertex) -,- - - [cnn3, cnn4, cnn5, cnn6] 14merge2 (MergeVertex) -,- - - [cnn3-stride2, cnn4-stride2, cnn5-stride2, cnn6-stride2] 15globalPool1 (GlobalPoolingLayer) -,- 0 - [merge1] 16globalPool2 (GlobalPoolingLayer) -,- 0 - [merge2] 17fc-merge (MergeVertex) -,- - - [globalPool1, globalPool2] 18fc (DenseLayer) 400,200 80200 W:{400,200}, b:{1,200} [fc-merge] 19out (OutputLayer) 200,15 3015 W:{200,15}, b:{1,15} [fc] 20----------------------------------------------------------------------------------------------------------------------------------------------- 21 Total Parameters: 263615 22 Trainable Parameters: 263615 23 Frozen Parameters: 0 24===============================================================================================================================================

 DL4J的UIServer界面如下,这里我给定的端口号为9001,打开web界面可以看到平均loss的详情,梯度更新的详情等

http://localhost:9001/train/overview

 七、掩模

    句子有长有短,CNN将如何处理呢?

    处理的办法其实很暴力,将一个minibatch中的最长句子找到,new出最大长度的张量,多余值用掩模掩掉即可,废话不多说,直接上代码

1 if(sentencesAlongHeight){ 2 featuresMask = Nd4j.create(currMinibatchSize, 1, maxLength, 1); 3 for (int i = 0; i < currMinibatchSize; i++) { 4 int sentenceLength = tokenizedSentences.get(i).getFirst().size(); 5 if (sentenceLength >= maxLength) { 6 featuresMask.slice(i).assign(1.0); 7 } else { 8 featuresMask.get(NDArrayIndex.point(i), NDArrayIndex.point(0), NDArrayIndex.interval(0, sentenceLength), NDArrayIndex.point(0)).assign(1.0); 9 } 10 } 11 } else { 12 featuresMask = Nd4j.create(currMinibatchSize, 1, 1, maxLength); 13 for (int i = 0; i < currMinibatchSize; i++) { 14 int sentenceLength = tokenizedSentences.get(i).getFirst().size(); 15 if (sentenceLength >= maxLength) { 16 featuresMask.slice(i).assign(1.0); 17 } else { 18 featuresMask.get(NDArrayIndex.point(i), NDArrayIndex.point(0), NDArrayIndex.point(0), NDArrayIndex.interval(0, sentenceLength)).assign(1.0); 19 } 20 } 21 }

    这里为什么有个if呢?生成句子张量的时候,可以任意指定句子的方向,可以沿着矩阵中height的方向,也可以是width的方向,方向不同,填掩模的那一维也就不同。

八、结果

    运行了10个Epoch结果如下:

1========================Evaluation Metrics======================== 2 # of classes: 15 3 Accuracy: 0.8420 4 Precision: 0.8362 (1 class excluded from average) 5 Recall: 0.7783 6 F1 Score: 0.8346 (1 class excluded from average) 7Precision, recall & F1: macro-averaged (equally weighted avg. of 15 classes) 8 9Warning: 1 class was never predicted by the model and was excluded from average precision 10Classes excluded from average precision: [12] 11 12=========================Confusion Matrix========================= 13 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 14---------------------------------------------------------------------------- 15 973 35 114 2 9 8 11 19 14 6 19 11 0 22 13 | 0 = 0 16 17 4636 250 37 51 16 14 151 47 29 232 36 0 82 44 | 1 = 1 17 103 176 6980 108 16 8 31 62 83 41 53 77 0 36 163 | 2 = 2 18 9 78 244 6692 37 9 52 59 33 27 57 54 0 10 96 | 3 = 3 19 7 52 36 31 4072 96 101 107 581 20 64 108 0 135 37 | 4 = 4 20 12 18 22 8 150 3061 27 36 53 2 100 16 0 56 2 | 5 = 5 21 17 38 71 26 94 13 6443 43 174 31 121 39 0 32 34 | 6 = 6 22 17 157 93 49 62 20 34 4793 85 14 58 36 0 49 31 | 7 = 7 23 1 45 71 21 436 30 195 138 7018 48 54 49 0 45 148 | 8 = 8 24 24 74 84 47 24 1 57 50 68 3963 45 431 0 9 65 | 9 = 9 25 9 165 90 21 40 37 61 40 42 21 3428 111 0 78 30 | 10 = 10 26 47 78 173 52 114 20 48 67 93 320 140 4097 0 48 29 | 11 = 11 27 0 0 0 0 60 0 1 0 5 0 0 0 0 0 0 | 12 = 12 28 35 105 31 6 139 37 34 61 79 11 153 35 0 3187 12 | 13 = 13 29 14 36 210 128 31 2 19 20 164 44 38 15 0 19 5183 | 14 = 14

    平均准确率0.8420,比原资源中给定的结果略好,F1 score要略差一点,混淆矩阵中,有一个类别,无法被预测到,是因为样本中改类别数据量本身很少,难以抓到共性特征。这里参数如果精心调节一番,迭代更多次数,理论上会有更好的表现。

九、后记    

    读Deeplearning4j是一种享受,优雅的架构,清晰的逻辑,多种设计模式,扩展性强,将有后续博客,对dl4j源码进行剖析。

快乐源于分享。

此博客乃作者原创, 转载请注明出处

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )