diff --git a/cs/unittest/TestJson.cs b/cs/unittest/TestJson.cs index 4026d6f5efc..199f0a75035 100644 --- a/cs/unittest/TestJson.cs +++ b/cs/unittest/TestJson.cs @@ -445,7 +445,7 @@ public void TestJsonRedirection() [TestCategory("Vowpal Wabbit/JSON")] public void TestDecisionServiceJson() { - using (var vw = new VowpalWabbit("--cb_adf")) + using (var vw = new VowpalWabbit("--cb_adf --dsjson")) { var json = @"{""EventId"":""abc"",""a"":[1,2,3],""Version"":""1"",""c"":{""u"":{""loc"":""New York""},""_multi"":[{""x"":[{""x"":{""cat"":""1""}},null,{""y"":{""cat"":""3""}}]},{""x"":{""cat"":""2""}}]},""p"":[0.8,0.1,0.1]}"; var obj = JsonConvert.DeserializeObject(json); @@ -489,7 +489,7 @@ public void TestDecisionServiceJson2() { var json = "{\"_skipLearn\":true,\"Version\":\"2\",\"EventId\":\"73369b13ec98433096a1496d27da0bfd\",\"a\":[9,11,13,6,4,5,12,1,2,10,8,3,7],\"c\":{\"_synthetic\":false,\"User\":{\"_age\":0},\"Geo\":{\"country\":\"United States\",\"_countrycf\":\"8\",\"state\":\"New Jersey\",\"city\":\"Somerdale\",\"_citycf\":\"5\",\"dma\":\"504\"},\"MRefer\":{\"referer\":\"http://www.complex.com/\"},\"OUserAgent\":{\"_ua\":\"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1\",\"_DeviceBrand\":\"Apple\",\"_DeviceFamily\":\"iPhone\",\"_DeviceIsSpider\":false,\"_DeviceModel\":\"iPhone\",\"_OSFamily\":\"iOS\",\"_OSMajor\":\"10\",\"_OSPatch\":\"2\",\"DeviceType\":\"Mobile\"},\"_multi\":[{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweater-conor-mcgregor-may-be-set\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweater-conor-mcgregor-may-be-set\"},\"j\":[{\"_title\":\"The Floyd Mayweather vs. Conor McGregor Fight Date Has Finally Been Announced\"},{\"RVisionTags\":{\"person\":0.999368966,\"man\":0.998108864,\"wearing\":0.9368642,\"hat\":0.928866565,\"indoor\":0.893332958,\"close\":0.201101109},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0148094837,\"racyScore\":0.0144806523},\"_expires\":\"2017-06-17T21:42:30.3186957Z\"},{\"Emotion0\":{\"anger\":1.81591489E-07,\"contempt\":9.946987E-06,\"disgust\":6.11135547E-05,\"fear\":4.565633E-12,\"happiness\":0.999928534,\"neutral\":2.28114558E-07,\"sadness\":3.07409E-09,\"surprise\":1.46155665E-08},\"_expires\":\"2017-06-17T21:42:28.8496462Z\"},{\"Tags\":{\"Floyd Mayweather Jr.\":0.982,\"Conor McGregor\":0.938,\"Complex\":0.334,\"Twitter Inc.\":0.997,\"Dan Mullane\":0.006,\"Mixed martial arts\":1,\"Net Controls\":0.281,\"Boxing\":1,\"Ontario\":1,\"Dana White\":0.972,\"Las Vegas Valley\":0.995,\"Nevada Athletic Commission\":0.024,\"Mayweather Promotions\":0.118,\"MGM Grand Garden Arena\":0.997,\"Fighting game\":0.641,\"Nevada\":1,\"Coming out\":0.076},\"_expires\":\"2017-06-17T21:42:29.8271823Z\"},{\"XSentiment\":2.93618323E-05,\"_expires\":\"2017-06-17T21:42:29.1777863Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/young-thug-beautiful-thugger-girls-violent-trailer-has-people-upset\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/young-thug-beautiful-thugger-girls-violent-trailer-has-people-upset\"},\"j\":[{\"_title\":\"Why Young Thug's Violent Trailer for 'Beautiful Thugger Girls' Has People Upset\"},{\"RVisionTags\":{\"person\":0.9621564,\"indoor\":0.93759197},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0590519942,\"racyScore\":0.0740057454},\"_expires\":\"2017-06-17T17:27:41.0750729Z\"},{\"Emotion0\":{\"anger\":0.000195411936,\"contempt\":0.0007970728,\"disgust\":7.29157255E-05,\"fear\":0.000106336483,\"happiness\":0.000127831052,\"neutral\":0.9209777,\"sadness\":0.0775285438,\"surprise\":0.000194183245},\"_expires\":\"2017-06-17T17:27:33.4982953Z\"},{\"Tags\":{\"Young Thug\":0.445,\"Beautiful\":0.005,\"Twitter Inc.\":1,\"Drake\":0.968,\"Instagram\":0.995,\"Breezy\":0.014,\"June 13\":0.008,\"Cover art\":0.003,\"Album\":0.99,\"Surface\":0.012,\"Prince Michael Jackson II\":1},\"_expires\":\"2017-06-17T17:27:33.4670277Z\"},{\"XSentiment\":2.46002031E-07,\"_expires\":\"2017-06-17T17:27:34.0139318Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/did-we-just-witness-peak-lebron\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/did-we-just-witness-peak-lebron\"},\"j\":[{\"_title\":\"Did We Just Witness Peak LeBron?\"},{\"RVisionTags\":{\"person\":0.9887383,\"indoor\":0.8961688},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0181511845,\"racyScore\":0.0380923077},\"_expires\":\"2017-06-17T14:46:55.4499648Z\"},{\"Emotion0\":{\"anger\":0.0007875018,\"contempt\":0.000600368367,\"disgust\":0.00288117747,\"fear\":0.00168624625,\"happiness\":4.59727671E-05,\"neutral\":0.377137423,\"sadness\":0.00264490047,\"surprise\":0.6142164},\"_expires\":\"2017-06-17T14:46:49.7072998Z\"},{\"Tags\":{\"LeBron James\":1,\"Complex\":0.08,\"Broadcasting of sports events\":0.12,\"Twitter Inc.\":0.999,\"USA Today\":0.994,\"Kyle Broflovski\":0.023,\"Superman\":0.694,\"Cleveland Cavaliers\":1,\"UNK NBA\":1,\"Bill Russell NBA Finals Most Valuable Player Award\":1,\"Golden State Warriors\":1,\"Michael Jordan\":1,\"Kobe Bryant\":1,\"Time\":0.865,\"Magic Johnson\":0.999},\"_expires\":\"2017-06-17T14:46:49.4416286Z\"},{\"XSentiment\":0.9999974,\"_expires\":\"2017-06-17T14:46:49.7541575Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-made-the-difference\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-made-the-difference\"},\"j\":[{\"_title\":\"Golden State (Probably) Would Have Blown Another Lead Without KD\"},{\"RVisionTags\":{\"person\":0.999950767,\"player\":0.984833837,\"sport\":0.9816471,\"athletic game\":0.9691899,\"basketball\":0.7260069,\"hand\":0.463383943,\"crowd\":0.3354485},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0127091147,\"racyScore\":0.01585682},\"_expires\":\"2017-06-17T14:44:47.7452593Z\"},{\"Emotion0\":{\"anger\":0.9830929,\"contempt\":6.38814454E-05,\"disgust\":0.0142428661,\"fear\":4.3443215E-05,\"happiness\":0.00127731345,\"neutral\":0.0003242454,\"sadness\":0.0006526729,\"surprise\":0.0003026811},\"_expires\":\"2017-06-17T14:44:47.1857099Z\"},{\"Tags\":{\"Golden State Warriors\":1,\"Lead guitar\":0.046,\"Kevin Durant\":1,\"Twitter Inc.\":0.985,\"Martinez\":0.015,\"Splash Brothers\":1,\"Cleveland Cavaliers\":1,\"Monday Night Football\":0.398,\"Richard Jefferson\":0.95,\"Kevin Love\":0.997,\"McDonald's All-American Game\":0.859,\"University of Texas at Austin College of Fine Arts\":0.999,\"Naismith College Player of the Year\":0.117,\"UNK NBA\":1,\"NBA Most Valuable Player Award\":1,\"Olympic Games\":0.858,\"NBA All-Star Game\":1,\"Champion\":0.12,\"Bill Russell NBA Finals Most Valuable Player Award\":1,\"Stephen Curry\":0.989,\"Draymond Green\":0.674,\"Seat Pleasant\":0.122},\"_expires\":\"2017-06-17T14:44:46.9264793Z\"},{\"XSentiment\":1,\"_expires\":\"2017-06-17T14:44:47.4639789Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\"},\"j\":[{\"_title\":\"Watch 2 Chainz Flex on Kendrick Lamar's \\\"DNA\\\" Beat in New Freestyle\"},{\"RVisionTags\":{\"person\":0.9840661,\"dressed\":0.304036647},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0285988934,\"racyScore\":0.0257010516},\"_expires\":\"2017-06-17T19:30:46.5524171Z\"},{\"_expires\":\"2017-06-17T19:30:46.2086571Z\"},{\"Tags\":{\"2 Chainz\":1,\"Kendrick Lamar\":1,\"DNA\":0.053,\"Hip hop production\":0.015,\"Freestyle rap\":0.934,\"Philadelphia\":0.193,\"Twitter Inc.\":1,\"Subscription business model\":0.011,\"Complex\":1,\"Los Angeles\":1,\"Georgia\":0.894,\"Trap\":0.579,\"Top Dawg Entertainment\":0.872,\"Virtual reality\":0.011,\"Travis Scott\":0.011,\"Collaboration\":0.004,\"Nicki Minaj\":0.999,\"Remy Ma\":0.99,\"Papoose\":0.892,\"Sampling\":0.017,\"Hip hop music\":1,\"Tha Carter V\":1,\"Everyday (ASAP Rocky song)\":0.003},\"_expires\":\"2017-06-17T19:30:46.1314041Z\"},{\"XSentiment\":1,\"_expires\":\"2017-06-17T19:30:46.7944713Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweather-viral-challenge-backfires-reacts\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweather-viral-challenge-backfires-reacts\"},\"j\":[{\"_title\":\"Floyd Mayweather Attempted to Start His Own Viral Challenge and It Hilariously Backfired\"},{\"RVisionTags\":{\"person\":0.999924064,\"man\":0.9552084,\"crowd\":0.01707872},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0114686647,\"racyScore\":0.0159121435},\"_expires\":\"2017-06-17T21:13:01.1925796Z\"},{\"Emotion0\":{\"anger\":0.00127403683,\"contempt\":0.0222781487,\"disgust\":0.0002176114,\"fear\":9.893078E-06,\"happiness\":0.07823167,\"neutral\":0.897131145,\"sadness\":0.00048493495,\"surprise\":0.0003725856},\"_expires\":\"2017-06-17T21:12:59.3669891Z\"},{\"Tags\":{\"Floyd Mayweather Jr.\":0.192,\"Philadelphia\":0.65,\"Twitter Inc.\":1,\"USA Today\":0.979,\"Sport\":0.85,\"Conor McGregor\":0.016,\"June 14\":0.009,\"Troy\":0.005,\"Honda Civic\":0.004,\"Bank account\":0.025,\"NASCAR on TNT\":0.099,\"Boxing\":1},\"_expires\":\"2017-06-17T21:12:59.3201526Z\"},{\"XSentiment\":0.999998,\"_expires\":\"2017-06-17T21:13:01.7647699Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-discusses-random-text-he-received-from-obama-after-winning-nba-finals\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-discusses-random-text-he-received-from-obama-after-winning-nba-finals\"},\"j\":[{\"_title\":\"Kevin Durant Discusses 'Random' Text He Received From Obama After Winning NBA Finals\"},{\"RVisionTags\":{\"person\":0.9939494,\"outdoor\":0.9162231,\"male\":0.242890328},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0208446719,\"racyScore\":0.0304868072},\"_expires\":\"2017-06-17T18:28:04.5675692Z\"},{\"Emotion0\":{\"anger\":2.35385942E-05,\"contempt\":3.294205E-06,\"disgust\":1.38025935E-05,\"fear\":7.8729E-06,\"happiness\":0.9997165,\"neutral\":0.0001607666,\"sadness\":1.25915176E-05,\"surprise\":6.161377E-05},\"Emotion1\":{\"anger\":0.00371822272,\"contempt\":0.000460597221,\"disgust\":0.000157746123,\"fear\":0.000275517668,\"happiness\":0.0403934456,\"neutral\":0.86978966,\"sadness\":0.08499769,\"surprise\":0.000207107121},\"_expires\":\"2017-06-17T18:28:03.3944386Z\"},{\"Tags\":{\"Kevin Durant\":1,\"Random House\":0.069,\"Barack Obama\":1,\"UNK NBA\":1,\"Twitter Inc.\":1,\"USA Today\":0.999,\"Sports journalism\":0.01,\"Cary\":0.025,\"The NBA Finals\":0.017,\"Monday Night Football\":0.943,\"Golden State Warriors\":1,\"Bill Simmons\":0.866,\"Podcast\":0.918,\"Oracle Arena\":0.028,\"Bill Russell NBA Finals Most Valuable Player Award\":1,\"June 13\":0.034,\"Cleveland Cavaliers\":1,\"LeBron James\":1,\"Kyrie Irving\":1,\"Allen Iverson\":0.999,\"Rihanna\":0.63,\"The League\":0.166,\"Stay\":0.858,\"Singing\":0.024,\"President of the United States\":1},\"_expires\":\"2017-06-17T18:28:03.8094333Z\"},{\"XSentiment\":0.9999663,\"_expires\":\"2017-06-17T18:28:03.9031587Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/pop-culture/2017/06/tj-miller-hbo-special-interview\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/pop-culture/2017/06/tj-miller-hbo-special-interview\"},\"j\":[{\"_title\":\"T.J. Miller's Done With 'Silicon Valley,' But His Career's Just Getting Started\"},{\"RVisionTags\":{\"person\":0.999560535,\"man\":0.9939658,\"suit\":0.950625,\"outdoor\":0.9169477,\"wearing\":0.7708228,\"jacket\":0.528340757,\"coat\":0.490623325,\"dark\":0.32463637,\"male\":0.212570518,\"microphone\":0.148984566,\"crowd\":0.0156043554},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0102083739,\"racyScore\":0.0126593616},\"_expires\":\"2017-06-17T18:59:23.2248419Z\"},{\"Emotion0\":{\"anger\":1.0740634E-05,\"contempt\":1.42603E-05,\"disgust\":5.26589356E-05,\"fear\":1.20671484E-06,\"happiness\":0.996862,\"neutral\":0.00302408962,\"sadness\":5.683868E-06,\"surprise\":2.93453577E-05},\"_expires\":\"2017-06-17T18:59:22.2148365Z\"},{\"Tags\":{\"T. J. Miller\":0.999,\"Silicon Valley\":0.999,\"Whitney\":0.264,\"Twitter Inc.\":1,\"HBO\":1,\"Complex\":0.032,\"The Gorburger Show\":0.062,\"Funny or Die\":0.914,\"Comedy Central\":1,\"Japan\":1,\"Ridiculousness\":0.004,\"Deadpool\":0.117,\"Cloverfield\":0.921,\"Cannes Film Festival\":0.987,\"Energizer Bunny\":0.007,\"Amy Schumer\":0.021,\"Pete Holmes\":0.153,\"Peter Boyle\":0.003,\"Downtown Los Angeles\":0.843,\"Supervillain\":0.038,\"San Francisco\":0.809,\"Jesus Christ\":0.902,\"Kong: Skull Island\":0.751,\"Jordan Vogt-Roberts\":0.979,\"Usher\":0.325,\"Flea\":0.536,\"Henry Rollins\":0.096,\"Federal government of the United States\":0.485,\"Mike Judge\":0.974,\"Uber\":0.404,\"Chelsea Handler\":0.364},\"_expires\":\"2017-06-17T18:59:22.7021704Z\"},{\"_expires\":\"2017-06-17T18:59:22.7489821Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/life/2017/06/iphone-8-edge-to-edge-screen\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/life/2017/06/iphone-8-edge-to-edge-screen\"},\"j\":[{\"_title\":\"Newly-Leaked Pictures Show You What iPhone 8 Screen Might Look Like\"},{\"RVisionTags\":{\"iPod\":0.802692235,\"electronics\":0.7313406},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0145561891,\"racyScore\":0.0151244327},\"_expires\":\"2017-06-17T23:14:19.5133399Z\"},{\"_expires\":\"2017-06-17T23:14:18.3277963Z\"},{\"Tags\":{\"iOS\":0.492,\"Complex\":0.102,\"Twitter Inc.\":1,\"Imgur\":0.996,\"Apple Inc.\":1,\"Reddit\":0.57,\"Check It Out\":0.003,\"China\":0.813,\"iPhone\":1},\"_expires\":\"2017-06-17T23:14:18.0245382Z\"},{\"XSentiment\":0.9994883,\"_expires\":\"2017-06-17T23:14:18.7364306Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/ryan-destiny-get-sweaty\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/ryan-destiny-get-sweaty\"},\"j\":[{\"_title\":\"Ryan Destiny Talks About Starring in Hit TV Series 'Star' on Get Sweaty With Emily Oberg\"},{\"RVisionTags\":{\"person\":0.998844266,\"boxing\":0.573501348},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0180808976,\"racyScore\":0.07170816},\"_expires\":\"2017-06-17T19:55:45.6659091Z\"},{\"Emotion0\":{\"anger\":0.0428811572,\"contempt\":0.0120455148,\"disgust\":0.00588818826,\"fear\":0.0031699785,\"happiness\":0.0325962044,\"neutral\":0.839067638,\"sadness\":0.0560076348,\"surprise\":0.00834368449},\"Emotion1\":{\"anger\":9.522394E-08,\"contempt\":3.42922242E-08,\"disgust\":3.1531672E-06,\"fear\":4.24115054E-09,\"happiness\":0.999994,\"neutral\":2.3093894E-06,\"sadness\":1.65964408E-07,\"surprise\":2.82413E-07},\"_expires\":\"2017-06-17T19:55:44.6508371Z\"},{\"Tags\":{\"Destiny\":0.299,\"HiT TV\":0.008,\"Complex\":0.953,\"Twitter Inc.\":1,\"New York City\":1,\"Robert E. Lee\":0.004,\"Fox Broadcasting Company\":1,\"Naomi Campbell\":0.091,\"Sy Kravitz\":0.731,\"Queen Latifah\":0.876},\"_expires\":\"2017-06-17T19:55:44.2710467Z\"},{\"XSentiment\":0.113136955,\"_expires\":\"2017-06-17T19:55:45.0269667Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\"},\"j\":[{\"_title\":\"Watch 2 Chainz Flex on Kendrick Lamar's \\\"DNA\\\" Beat in New Freestyle\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/everyday-struggle-ep39-kehlani-tinashe\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/everyday-struggle-ep39-kehlani-tinashe\"},\"j\":[{\"_title\":\"Joe Budden and DJ Akademiks Discuss Tinashe Controversy and Kehlani Cussing Out Heckler on 'Everyday Struggle'\"},{\"RVisionTags\":{\"abstract\":0.5319324},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.09698458,\"racyScore\":0.08712957},\"_expires\":\"2017-06-17T17:56:47.5354099Z\"},{\"Emotion0\":{\"anger\":0.0121765705,\"contempt\":0.0152475182,\"disgust\":0.0186378732,\"fear\":0.00241010683,\"happiness\":0.202662408,\"neutral\":0.6867056,\"sadness\":0.0549196824,\"surprise\":0.00724024652},\"Emotion1\":{\"anger\":0.005693211,\"contempt\":0.0003093396,\"disgust\":0.000264122762,\"fear\":0.000122387908,\"happiness\":0.000162528377,\"neutral\":0.9901545,\"sadness\":0.00110488839,\"surprise\":0.00218904181},\"_expires\":\"2017-06-17T17:56:46.5115225Z\"},{\"Tags\":{\"Joe Budden\":0.972,\"Disc jockey\":0.998,\"Tinashe\":0.485,\"Kehlani\":0.011,\"Profanity\":0.006,\"Heckler\":0.003,\"Complex\":0.992,\"Twitter Inc.\":0.959,\"XXL\":0.906,\"Kyrie Irving\":0.277,\"LeBron James\":0.998,\"Michael Jordan\":0.989,\"Floyd Mayweather Jr.\":0.031,\"Conor McGregor\":0.276},\"_expires\":\"2017-06-17T17:56:46.0896119Z\"},{\"XSentiment\":0.005011654,\"_expires\":\"2017-06-17T17:56:46.6990201Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/lonzo-ball-interview\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/lonzo-ball-interview\"},\"j\":[{\"_title\":\"Lonzo Ball Finally Told Us How He Really Feels About LaVar's Media Antics\"},{\"RVisionTags\":{\"person\":0.9992661,\"sport\":0.9894762,\"athletic game\":0.9814596,\"basketball\":0.9540427,\"player\":0.897939742,\"crowd\":0.529209554,\"watching\":0.450753957},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0182933789,\"racyScore\":0.0168850645},\"_expires\":\"2017-06-17T14:11:20.108671Z\"},{\"Emotion0\":{\"anger\":6.044781E-05,\"contempt\":2.85142833E-05,\"disgust\":4.95703644E-05,\"fear\":0.008861069,\"happiness\":2.874653E-05,\"neutral\":0.8961255,\"sadness\":0.00528799742,\"surprise\":0.08955817},\"_expires\":\"2017-06-17T14:11:19.6899531Z\"},{\"Tags\":{\"Los Angeles Angels of Anaheim\":0.515,\"Songwriter\":0.33,\"Twitter Inc.\":1,\"USA Today\":1,\"Broadcasting of sports events\":0.038,\"Chino Hills\":0.002,\"UCLA Bruins men's basketball\":0.6,\"NCAA Men's Division I Basketball Championship\":0.897,\"Sweet\":0.019,\"Todd Marinovich\":0.938,\"Marv Albert\":0.055,\"UNK NBA\":1,\"Fox Broadcasting Company\":0.933,\"Jayson Tatum\":0.004,\"ZO2\":0.008,\"Los Angeles Lakers\":1,\"Lamar Odom\":0.986,\"Lamar Cardinals Men's Basketball\":0.004,\"Magic Johnson\":0.995,\"Jason Kidd\":1,\"LeBron James\":1,\"James Harden\":0.99,\"Adidas\":0.811,\"Puerto Rico\":0.012,\"Stephen Curry\":0.981,\"Michael Jordan\":1,\"Shaquille O'Neal\":1},\"_expires\":\"2017-06-17T14:11:19.6587216Z\"},{\"XSentiment\":1,\"_expires\":\"2017-06-17T14:11:20.0892306Z\"}]}]},\"p\":[0.8153846,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154],\"VWState\":{\"m\":\"decc63fa2c284ec9887ee0572ea16d17/7860031216114d8bb718c40abc801bf4\"}}"; - using (var vw = new VowpalWabbit("--cb_adf")) + using (var vw = new VowpalWabbit("--cb_adf --dsjson")) { var obj = JsonConvert.DeserializeObject(json); var bytes = new byte[Encoding.UTF8.GetMaxByteCount(json.Length) + 1]; @@ -529,7 +529,7 @@ public void TestDecisionServiceJson_CopyJson() { var json = "{\"Version\":\"2\",\"EventId\":\"73369b13ec98433096a1496d27da0bfd\",\"a\":[9,11,13,6,4,5,12,1,2,10,8,3,7],\"c\":{\"_synthetic\":false,\"User\":{\"_age\":0},\"Geo\":{\"country\":\"United States\",\"_countrycf\":\"8\",\"state\":\"New Jersey\",\"city\":\"Somerdale\",\"_citycf\":\"5\",\"dma\":\"504\"},\"MRefer\":{\"referer\":\"http://www.complex.com/\"},\"OUserAgent\":{\"_ua\":\"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1\",\"_DeviceBrand\":\"Apple\",\"_DeviceFamily\":\"iPhone\",\"_DeviceIsSpider\":false,\"_DeviceModel\":\"iPhone\",\"_OSFamily\":\"iOS\",\"_OSMajor\":\"10\",\"_OSPatch\":\"2\",\"DeviceType\":\"Mobile\"},\"_multi\":[{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweater-conor-mcgregor-may-be-set\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweater-conor-mcgregor-may-be-set\"},\"j\":[{\"_title\":\"The Floyd Mayweather vs. Conor McGregor Fight Date Has Finally Been Announced\"},{\"RVisionTags\":{\"person\":0.999368966,\"man\":0.998108864,\"wearing\":0.9368642,\"hat\":0.928866565,\"indoor\":0.893332958,\"close\":0.201101109},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0148094837,\"racyScore\":0.0144806523},\"_expires\":\"2017-06-17T21:42:30.3186957Z\"},{\"Emotion0\":{\"anger\":1.81591489E-07,\"contempt\":9.946987E-06,\"disgust\":6.11135547E-05,\"fear\":4.565633E-12,\"happiness\":0.999928534,\"neutral\":2.28114558E-07,\"sadness\":3.07409E-09,\"surprise\":1.46155665E-08},\"_expires\":\"2017-06-17T21:42:28.8496462Z\"},{\"Tags\":{\"Floyd Mayweather Jr.\":0.982,\"Conor McGregor\":0.938,\"Complex\":0.334,\"Twitter Inc.\":0.997,\"Dan Mullane\":0.006,\"Mixed martial arts\":1,\"Net Controls\":0.281,\"Boxing\":1,\"Ontario\":1,\"Dana White\":0.972,\"Las Vegas Valley\":0.995,\"Nevada Athletic Commission\":0.024,\"Mayweather Promotions\":0.118,\"MGM Grand Garden Arena\":0.997,\"Fighting game\":0.641,\"Nevada\":1,\"Coming out\":0.076},\"_expires\":\"2017-06-17T21:42:29.8271823Z\"},{\"XSentiment\":2.93618323E-05,\"_expires\":\"2017-06-17T21:42:29.1777863Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/young-thug-beautiful-thugger-girls-violent-trailer-has-people-upset\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/young-thug-beautiful-thugger-girls-violent-trailer-has-people-upset\"},\"j\":[{\"_title\":\"Why Young Thug's Violent Trailer for 'Beautiful Thugger Girls' Has People Upset\"},{\"RVisionTags\":{\"person\":0.9621564,\"indoor\":0.93759197},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0590519942,\"racyScore\":0.0740057454},\"_expires\":\"2017-06-17T17:27:41.0750729Z\"},{\"Emotion0\":{\"anger\":0.000195411936,\"contempt\":0.0007970728,\"disgust\":7.29157255E-05,\"fear\":0.000106336483,\"happiness\":0.000127831052,\"neutral\":0.9209777,\"sadness\":0.0775285438,\"surprise\":0.000194183245},\"_expires\":\"2017-06-17T17:27:33.4982953Z\"},{\"Tags\":{\"Young Thug\":0.445,\"Beautiful\":0.005,\"Twitter Inc.\":1,\"Drake\":0.968,\"Instagram\":0.995,\"Breezy\":0.014,\"June 13\":0.008,\"Cover art\":0.003,\"Album\":0.99,\"Surface\":0.012,\"Prince Michael Jackson II\":1},\"_expires\":\"2017-06-17T17:27:33.4670277Z\"},{\"XSentiment\":2.46002031E-07,\"_expires\":\"2017-06-17T17:27:34.0139318Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/did-we-just-witness-peak-lebron\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/did-we-just-witness-peak-lebron\"},\"j\":[{\"_title\":\"Did We Just Witness Peak LeBron?\"},{\"RVisionTags\":{\"person\":0.9887383,\"indoor\":0.8961688},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0181511845,\"racyScore\":0.0380923077},\"_expires\":\"2017-06-17T14:46:55.4499648Z\"},{\"Emotion0\":{\"anger\":0.0007875018,\"contempt\":0.000600368367,\"disgust\":0.00288117747,\"fear\":0.00168624625,\"happiness\":4.59727671E-05,\"neutral\":0.377137423,\"sadness\":0.00264490047,\"surprise\":0.6142164},\"_expires\":\"2017-06-17T14:46:49.7072998Z\"},{\"Tags\":{\"LeBron James\":1,\"Complex\":0.08,\"Broadcasting of sports events\":0.12,\"Twitter Inc.\":0.999,\"USA Today\":0.994,\"Kyle Broflovski\":0.023,\"Superman\":0.694,\"Cleveland Cavaliers\":1,\"UNK NBA\":1,\"Bill Russell NBA Finals Most Valuable Player Award\":1,\"Golden State Warriors\":1,\"Michael Jordan\":1,\"Kobe Bryant\":1,\"Time\":0.865,\"Magic Johnson\":0.999},\"_expires\":\"2017-06-17T14:46:49.4416286Z\"},{\"XSentiment\":0.9999974,\"_expires\":\"2017-06-17T14:46:49.7541575Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-made-the-difference\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-made-the-difference\"},\"j\":[{\"_title\":\"Golden State (Probably) Would Have Blown Another Lead Without KD\"},{\"RVisionTags\":{\"person\":0.999950767,\"player\":0.984833837,\"sport\":0.9816471,\"athletic game\":0.9691899,\"basketball\":0.7260069,\"hand\":0.463383943,\"crowd\":0.3354485},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0127091147,\"racyScore\":0.01585682},\"_expires\":\"2017-06-17T14:44:47.7452593Z\"},{\"Emotion0\":{\"anger\":0.9830929,\"contempt\":6.38814454E-05,\"disgust\":0.0142428661,\"fear\":4.3443215E-05,\"happiness\":0.00127731345,\"neutral\":0.0003242454,\"sadness\":0.0006526729,\"surprise\":0.0003026811},\"_expires\":\"2017-06-17T14:44:47.1857099Z\"},{\"Tags\":{\"Golden State Warriors\":1,\"Lead guitar\":0.046,\"Kevin Durant\":1,\"Twitter Inc.\":0.985,\"Martinez\":0.015,\"Splash Brothers\":1,\"Cleveland Cavaliers\":1,\"Monday Night Football\":0.398,\"Richard Jefferson\":0.95,\"Kevin Love\":0.997,\"McDonald's All-American Game\":0.859,\"University of Texas at Austin College of Fine Arts\":0.999,\"Naismith College Player of the Year\":0.117,\"UNK NBA\":1,\"NBA Most Valuable Player Award\":1,\"Olympic Games\":0.858,\"NBA All-Star Game\":1,\"Champion\":0.12,\"Bill Russell NBA Finals Most Valuable Player Award\":1,\"Stephen Curry\":0.989,\"Draymond Green\":0.674,\"Seat Pleasant\":0.122},\"_expires\":\"2017-06-17T14:44:46.9264793Z\"},{\"XSentiment\":1,\"_expires\":\"2017-06-17T14:44:47.4639789Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\"},\"j\":[{\"_title\":\"Watch 2 Chainz Flex on Kendrick Lamar's \\\"DNA\\\" Beat in New Freestyle\"},{\"RVisionTags\":{\"person\":0.9840661,\"dressed\":0.304036647},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0285988934,\"racyScore\":0.0257010516},\"_expires\":\"2017-06-17T19:30:46.5524171Z\"},{\"_expires\":\"2017-06-17T19:30:46.2086571Z\"},{\"Tags\":{\"2 Chainz\":1,\"Kendrick Lamar\":1,\"DNA\":0.053,\"Hip hop production\":0.015,\"Freestyle rap\":0.934,\"Philadelphia\":0.193,\"Twitter Inc.\":1,\"Subscription business model\":0.011,\"Complex\":1,\"Los Angeles\":1,\"Georgia\":0.894,\"Trap\":0.579,\"Top Dawg Entertainment\":0.872,\"Virtual reality\":0.011,\"Travis Scott\":0.011,\"Collaboration\":0.004,\"Nicki Minaj\":0.999,\"Remy Ma\":0.99,\"Papoose\":0.892,\"Sampling\":0.017,\"Hip hop music\":1,\"Tha Carter V\":1,\"Everyday (ASAP Rocky song)\":0.003},\"_expires\":\"2017-06-17T19:30:46.1314041Z\"},{\"XSentiment\":1,\"_expires\":\"2017-06-17T19:30:46.7944713Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweather-viral-challenge-backfires-reacts\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/floyd-mayweather-viral-challenge-backfires-reacts\"},\"j\":[{\"_title\":\"Floyd Mayweather Attempted to Start His Own Viral Challenge and It Hilariously Backfired\"},{\"RVisionTags\":{\"person\":0.999924064,\"man\":0.9552084,\"crowd\":0.01707872},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0114686647,\"racyScore\":0.0159121435},\"_expires\":\"2017-06-17T21:13:01.1925796Z\"},{\"Emotion0\":{\"anger\":0.00127403683,\"contempt\":0.0222781487,\"disgust\":0.0002176114,\"fear\":9.893078E-06,\"happiness\":0.07823167,\"neutral\":0.897131145,\"sadness\":0.00048493495,\"surprise\":0.0003725856},\"_expires\":\"2017-06-17T21:12:59.3669891Z\"},{\"Tags\":{\"Floyd Mayweather Jr.\":0.192,\"Philadelphia\":0.65,\"Twitter Inc.\":1,\"USA Today\":0.979,\"Sport\":0.85,\"Conor McGregor\":0.016,\"June 14\":0.009,\"Troy\":0.005,\"Honda Civic\":0.004,\"Bank account\":0.025,\"NASCAR on TNT\":0.099,\"Boxing\":1},\"_expires\":\"2017-06-17T21:12:59.3201526Z\"},{\"XSentiment\":0.999998,\"_expires\":\"2017-06-17T21:13:01.7647699Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-discusses-random-text-he-received-from-obama-after-winning-nba-finals\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/kevin-durant-discusses-random-text-he-received-from-obama-after-winning-nba-finals\"},\"j\":[{\"_title\":\"Kevin Durant Discusses 'Random' Text He Received From Obama After Winning NBA Finals\"},{\"RVisionTags\":{\"person\":0.9939494,\"outdoor\":0.9162231,\"male\":0.242890328},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0208446719,\"racyScore\":0.0304868072},\"_expires\":\"2017-06-17T18:28:04.5675692Z\"},{\"Emotion0\":{\"anger\":2.35385942E-05,\"contempt\":3.294205E-06,\"disgust\":1.38025935E-05,\"fear\":7.8729E-06,\"happiness\":0.9997165,\"neutral\":0.0001607666,\"sadness\":1.25915176E-05,\"surprise\":6.161377E-05},\"Emotion1\":{\"anger\":0.00371822272,\"contempt\":0.000460597221,\"disgust\":0.000157746123,\"fear\":0.000275517668,\"happiness\":0.0403934456,\"neutral\":0.86978966,\"sadness\":0.08499769,\"surprise\":0.000207107121},\"_expires\":\"2017-06-17T18:28:03.3944386Z\"},{\"Tags\":{\"Kevin Durant\":1,\"Random House\":0.069,\"Barack Obama\":1,\"UNK NBA\":1,\"Twitter Inc.\":1,\"USA Today\":0.999,\"Sports journalism\":0.01,\"Cary\":0.025,\"The NBA Finals\":0.017,\"Monday Night Football\":0.943,\"Golden State Warriors\":1,\"Bill Simmons\":0.866,\"Podcast\":0.918,\"Oracle Arena\":0.028,\"Bill Russell NBA Finals Most Valuable Player Award\":1,\"June 13\":0.034,\"Cleveland Cavaliers\":1,\"LeBron James\":1,\"Kyrie Irving\":1,\"Allen Iverson\":0.999,\"Rihanna\":0.63,\"The League\":0.166,\"Stay\":0.858,\"Singing\":0.024,\"President of the United States\":1},\"_expires\":\"2017-06-17T18:28:03.8094333Z\"},{\"XSentiment\":0.9999663,\"_expires\":\"2017-06-17T18:28:03.9031587Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/pop-culture/2017/06/tj-miller-hbo-special-interview\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/pop-culture/2017/06/tj-miller-hbo-special-interview\"},\"j\":[{\"_title\":\"T.J. Miller's Done With 'Silicon Valley,' But His Career's Just Getting Started\"},{\"RVisionTags\":{\"person\":0.999560535,\"man\":0.9939658,\"suit\":0.950625,\"outdoor\":0.9169477,\"wearing\":0.7708228,\"jacket\":0.528340757,\"coat\":0.490623325,\"dark\":0.32463637,\"male\":0.212570518,\"microphone\":0.148984566,\"crowd\":0.0156043554},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0102083739,\"racyScore\":0.0126593616},\"_expires\":\"2017-06-17T18:59:23.2248419Z\"},{\"Emotion0\":{\"anger\":1.0740634E-05,\"contempt\":1.42603E-05,\"disgust\":5.26589356E-05,\"fear\":1.20671484E-06,\"happiness\":0.996862,\"neutral\":0.00302408962,\"sadness\":5.683868E-06,\"surprise\":2.93453577E-05},\"_expires\":\"2017-06-17T18:59:22.2148365Z\"},{\"Tags\":{\"T. J. Miller\":0.999,\"Silicon Valley\":0.999,\"Whitney\":0.264,\"Twitter Inc.\":1,\"HBO\":1,\"Complex\":0.032,\"The Gorburger Show\":0.062,\"Funny or Die\":0.914,\"Comedy Central\":1,\"Japan\":1,\"Ridiculousness\":0.004,\"Deadpool\":0.117,\"Cloverfield\":0.921,\"Cannes Film Festival\":0.987,\"Energizer Bunny\":0.007,\"Amy Schumer\":0.021,\"Pete Holmes\":0.153,\"Peter Boyle\":0.003,\"Downtown Los Angeles\":0.843,\"Supervillain\":0.038,\"San Francisco\":0.809,\"Jesus Christ\":0.902,\"Kong: Skull Island\":0.751,\"Jordan Vogt-Roberts\":0.979,\"Usher\":0.325,\"Flea\":0.536,\"Henry Rollins\":0.096,\"Federal government of the United States\":0.485,\"Mike Judge\":0.974,\"Uber\":0.404,\"Chelsea Handler\":0.364},\"_expires\":\"2017-06-17T18:59:22.7021704Z\"},{\"_expires\":\"2017-06-17T18:59:22.7489821Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/life/2017/06/iphone-8-edge-to-edge-screen\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/life/2017/06/iphone-8-edge-to-edge-screen\"},\"j\":[{\"_title\":\"Newly-Leaked Pictures Show You What iPhone 8 Screen Might Look Like\"},{\"RVisionTags\":{\"iPod\":0.802692235,\"electronics\":0.7313406},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0145561891,\"racyScore\":0.0151244327},\"_expires\":\"2017-06-17T23:14:19.5133399Z\"},{\"_expires\":\"2017-06-17T23:14:18.3277963Z\"},{\"Tags\":{\"iOS\":0.492,\"Complex\":0.102,\"Twitter Inc.\":1,\"Imgur\":0.996,\"Apple Inc.\":1,\"Reddit\":0.57,\"Check It Out\":0.003,\"China\":0.813,\"iPhone\":1},\"_expires\":\"2017-06-17T23:14:18.0245382Z\"},{\"XSentiment\":0.9994883,\"_expires\":\"2017-06-17T23:14:18.7364306Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/ryan-destiny-get-sweaty\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/ryan-destiny-get-sweaty\"},\"j\":[{\"_title\":\"Ryan Destiny Talks About Starring in Hit TV Series 'Star' on Get Sweaty With Emily Oberg\"},{\"RVisionTags\":{\"person\":0.998844266,\"boxing\":0.573501348},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0180808976,\"racyScore\":0.07170816},\"_expires\":\"2017-06-17T19:55:45.6659091Z\"},{\"Emotion0\":{\"anger\":0.0428811572,\"contempt\":0.0120455148,\"disgust\":0.00588818826,\"fear\":0.0031699785,\"happiness\":0.0325962044,\"neutral\":0.839067638,\"sadness\":0.0560076348,\"surprise\":0.00834368449},\"Emotion1\":{\"anger\":9.522394E-08,\"contempt\":3.42922242E-08,\"disgust\":3.1531672E-06,\"fear\":4.24115054E-09,\"happiness\":0.999994,\"neutral\":2.3093894E-06,\"sadness\":1.65964408E-07,\"surprise\":2.82413E-07},\"_expires\":\"2017-06-17T19:55:44.6508371Z\"},{\"Tags\":{\"Destiny\":0.299,\"HiT TV\":0.008,\"Complex\":0.953,\"Twitter Inc.\":1,\"New York City\":1,\"Robert E. Lee\":0.004,\"Fox Broadcasting Company\":1,\"Naomi Campbell\":0.091,\"Sy Kravitz\":0.731,\"Queen Latifah\":0.876},\"_expires\":\"2017-06-17T19:55:44.2710467Z\"},{\"XSentiment\":0.113136955,\"_expires\":\"2017-06-17T19:55:45.0269667Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/2-chainz-dna-freestyle-kendrick-lamar\"},\"j\":[{\"_title\":\"Watch 2 Chainz Flex on Kendrick Lamar's \\\"DNA\\\" Beat in New Freestyle\"}]},{\"_tag\":\"cmplx$http://www.complex.com/music/2017/06/everyday-struggle-ep39-kehlani-tinashe\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/music/2017/06/everyday-struggle-ep39-kehlani-tinashe\"},\"j\":[{\"_title\":\"Joe Budden and DJ Akademiks Discuss Tinashe Controversy and Kehlani Cussing Out Heckler on 'Everyday Struggle'\"},{\"RVisionTags\":{\"abstract\":0.5319324},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.09698458,\"racyScore\":0.08712957},\"_expires\":\"2017-06-17T17:56:47.5354099Z\"},{\"Emotion0\":{\"anger\":0.0121765705,\"contempt\":0.0152475182,\"disgust\":0.0186378732,\"fear\":0.00241010683,\"happiness\":0.202662408,\"neutral\":0.6867056,\"sadness\":0.0549196824,\"surprise\":0.00724024652},\"Emotion1\":{\"anger\":0.005693211,\"contempt\":0.0003093396,\"disgust\":0.000264122762,\"fear\":0.000122387908,\"happiness\":0.000162528377,\"neutral\":0.9901545,\"sadness\":0.00110488839,\"surprise\":0.00218904181},\"_expires\":\"2017-06-17T17:56:46.5115225Z\"},{\"Tags\":{\"Joe Budden\":0.972,\"Disc jockey\":0.998,\"Tinashe\":0.485,\"Kehlani\":0.011,\"Profanity\":0.006,\"Heckler\":0.003,\"Complex\":0.992,\"Twitter Inc.\":0.959,\"XXL\":0.906,\"Kyrie Irving\":0.277,\"LeBron James\":0.998,\"Michael Jordan\":0.989,\"Floyd Mayweather Jr.\":0.031,\"Conor McGregor\":0.276},\"_expires\":\"2017-06-17T17:56:46.0896119Z\"},{\"XSentiment\":0.005011654,\"_expires\":\"2017-06-17T17:56:46.6990201Z\"}]},{\"_tag\":\"cmplx$http://www.complex.com/sports/2017/06/lonzo-ball-interview\",\"i\":{\"constant\":1,\"id\":\"cmplx$http://www.complex.com/sports/2017/06/lonzo-ball-interview\"},\"j\":[{\"_title\":\"Lonzo Ball Finally Told Us How He Really Feels About LaVar's Media Antics\"},{\"RVisionTags\":{\"person\":0.9992661,\"sport\":0.9894762,\"athletic game\":0.9814596,\"basketball\":0.9540427,\"player\":0.897939742,\"crowd\":0.529209554,\"watching\":0.450753957},\"SVisionAdult\":{\"isAdultContent\":false,\"isRacyContent\":false,\"adultScore\":0.0182933789,\"racyScore\":0.0168850645},\"_expires\":\"2017-06-17T14:11:20.108671Z\"},{\"Emotion0\":{\"anger\":6.044781E-05,\"contempt\":2.85142833E-05,\"disgust\":4.95703644E-05,\"fear\":0.008861069,\"happiness\":2.874653E-05,\"neutral\":0.8961255,\"sadness\":0.00528799742,\"surprise\":0.08955817},\"_expires\":\"2017-06-17T14:11:19.6899531Z\"},{\"Tags\":{\"Los Angeles Angels of Anaheim\":0.515,\"Songwriter\":0.33,\"Twitter Inc.\":1,\"USA Today\":1,\"Broadcasting of sports events\":0.038,\"Chino Hills\":0.002,\"UCLA Bruins men's basketball\":0.6,\"NCAA Men's Division I Basketball Championship\":0.897,\"Sweet\":0.019,\"Todd Marinovich\":0.938,\"Marv Albert\":0.055,\"UNK NBA\":1,\"Fox Broadcasting Company\":0.933,\"Jayson Tatum\":0.004,\"ZO2\":0.008,\"Los Angeles Lakers\":1,\"Lamar Odom\":0.986,\"Lamar Cardinals Men's Basketball\":0.004,\"Magic Johnson\":0.995,\"Jason Kidd\":1,\"LeBron James\":1,\"James Harden\":0.99,\"Adidas\":0.811,\"Puerto Rico\":0.012,\"Stephen Curry\":0.981,\"Michael Jordan\":1,\"Shaquille O'Neal\":1},\"_expires\":\"2017-06-17T14:11:19.6587216Z\"},{\"XSentiment\":1,\"_expires\":\"2017-06-17T14:11:20.0892306Z\"}]}]},\"p\":[0.8153846,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154],\"VWState\":{\"m\":\"decc63fa2c284ec9887ee0572ea16d17/7860031216114d8bb718c40abc801bf4\"}}"; - using (var vw = new VowpalWabbit("--cb_adf")) + using (var vw = new VowpalWabbit("--cb_adf --dsjson")) { var obj = JsonConvert.DeserializeObject(json); var bytes = new byte[Encoding.UTF8.GetMaxByteCount(json.Length) + 1]; @@ -567,9 +567,9 @@ public void TestDecisionServiceJson_CopyJson() [TestCategory("Vowpal Wabbit/JSON")] public void TestDecisionServiceJsonNull() { - var json = @"{""Version"":""1"",""EventId"":""7cacacea2c6e49b5b922f6f517a325ed"",""a"":[9,4,13,10,8,5,2,3,12,11,7,6,1],""c"":{""_synthetic"":false,""User"":{""_age"":0},""Geo"":{""country"":""United States"",""_countrycf"":""8"",""state"":""Georgia"",""city"":""Stone Mountain"",""_citycf"":""5"",""dma"":""524""},""MRefer"":{""referer"":""http://www.complex.com/""},""OUserAgent"":{""_ua"":""Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4"",""_DeviceBrand"":"""",""_DeviceFamily"":""Other"",""_DeviceIsSpider"":false,""_DeviceModel"":"""",""_OSFamily"":""Mac OS X"",""_OSMajor"":""10"",""_OSPatch"":""5"",""DeviceType"":""Desktop""},""_multi"":[{""_tag"":""cmplx$http://www.complex.com/music/2017/06/prodigy-mobb-deep-once-in-a-generation-rapper"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/prodigy-mobb-deep-once-in-a-generation-rapper""},""j"":[{""_title"":""Why Prodigy Was A Once-In-A-Generation Rapper""},{""RVisionTags"":{""person"":0.9913805,""hat"":0.6433856,""male"":0.153918922},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0162594952,""racyScore"":0.0152094},""TVisionCelebrities"":{""Prodigy"":0.9999119},""_expires"":""2017-06-24T22:43:00.9241929Z""},{""Emotion0"":{""anger"":0.005261584,""contempt"":0.01940289,""disgust"":0.00146069494,""fear"":7.486289E-05,""happiness"":0.0102698216,""neutral"":0.9544214,""sadness"":0.00859182,""surprise"":0.00051694276},""_expires"":""2017-06-24T22:43:00.0008415Z""},{""Tags"":{""Roc Marciano"":0.015,""Mobb Deep"":1,""Prodigy"":1,""Havoc"":1,""A Tribe Called Quest"":0.969,""Q-Tip"":0.992,""The Infamous"":1,""The Crystals"":0.01,""Fifth Beatle"":0.099},""_expires"":""2017-06-24T22:43:00.1727355Z""},{""XSentiment"":3.01036973E-13,""_expires"":""2017-06-24T22:43:00.9398236Z""}]},{""_tag"":""cmplx$http://www.complex.com/pop-culture/2017/06/best-movies-2017"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/pop-culture/2017/06/best-movies-2017""},""j"":[{""_title"":""The Best Movies of 2017 (So Far)""},{""RVisionTags"":{""text"":0.9992092,""book"":0.997986436},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0431842171,""racyScore"":0.0587232448},""_expires"":""2017-06-25T03:08:01.2111373Z""},{""Emotion0"":{""anger"":0.005728849,""contempt"":0.00117533945,""disgust"":8.215821E-05,""fear"":1.32827354E-05,""happiness"":0.000487715733,""neutral"":0.9911194,""sadness"":0.000120451317,""surprise"":0.00127282448},""_expires"":""2017-06-25T03:08:00.8636383Z""},null,{""XSentiment"":1,""_expires"":""2017-06-25T03:08:01.5112947Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak""},""j"":[{""_title"":""Rihanna Responds to DM From Fan Seeking Advice on Getting Over His First Heartbreak""},{""RVisionTags"":{""person"":0.99633044},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.013516671,""racyScore"":0.048148632},""_expires"":""2017-06-25T02:01:20.2363494Z""},{""Emotion0"":{""anger"":3.085194E-06,""contempt"":0.000411317043,""disgust"":1.026042E-05,""fear"":3.766979E-07,""happiness"":0.9783716,""neutral"":0.0211082119,""sadness"":5.66137569E-05,""surprise"":3.850023E-05},""_expires"":""2017-06-25T02:01:19.7050726Z""},{""Tags"":{""Rihanna"":1,""Martinez"":0.021,""Complex"":0.834,""Twitter Inc."":1,""If You"":0.003,""Grammy Awards"":1,""Mathematics"":0.999,""Malawi"":0.298,""Christine Teigen"":0.009,""Dave Chappelle"":0.047,""Presenter"":0.892,""Kendrick Lamar"":0.984,""Diamonds"":0.998},""_expires"":""2017-06-25T02:01:19.4155039Z""},{""XSentiment"":1,""_expires"":""2017-06-25T02:01:24.9467431Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak""},""j"":[{""_title"":""Rihanna Responds to DM From Fan Seeking Advice on Getting Over His First Heartbreak""}]},{""_tag"":""cmplx$http://www.complex.com/life/2017/06/guy-changes-from-shorts-to-dress-after-getting-sent-home-from-work-on-hot-day"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/life/2017/06/guy-changes-from-shorts-to-dress-after-getting-sent-home-from-work-on-hot-day""},""j"":[{""_title"":""Guy Sent Home by Boss for Wearing Shorts on a Hot Day, Returns to Work in Mom's Dress""},{""RVisionTags"":{""person"":0.999545038,""indoor"":0.998509467,""wall"":0.997952163},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.034187492,""racyScore"":0.0335518233},""_expires"":""2017-06-24T21:27:46.7135793Z""},{""Emotion0"":{""anger"":7.7880286E-05,""contempt"":0.00116215891,""disgust"":4.65850326E-06,""fear"":3.797253E-06,""happiness"":2.82076635E-05,""neutral"":0.986993253,""sadness"":0.0116343917,""surprise"":9.564323E-05},""_expires"":""2017-06-24T21:27:46.1197986Z""},{""Tags"":{""Boss Corporation"":0.007,""Shorts"":0.139,""Complex"":0.169,""Twitter Inc."":1,""English"":0.603,""The Daily Mirror"":0.008,""Oklahoma"":0.948,""High school"":0.999,""Lists of National Basketball Association players"":0.072,""UNK NBA"":0.529,""NBA dress code"":0.031,""Dress code"":0.113},""_expires"":""2017-06-24T21:27:46.0729078Z""},{""XSentiment"":0.9995655,""_expires"":""2017-06-24T21:27:46.7448359Z""}]},{""_tag"":""cmplx$http://www.complex.com/pop-culture/2017/06/tom-cruise-allegedly-balanced-bible-study-and-blow-jobs-away-from-risky-business-set"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/pop-culture/2017/06/tom-cruise-allegedly-balanced-bible-study-and-blow-jobs-away-from-risky-business-set""},""j"":[{""_title"":""Tom Cruise Was Allegedly Balancing Bible Study and Blow Jobs Away From the 'Risky Business' Set""},{""RVisionTags"":{""person"":0.9992724,""man"":0.9368908},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.009141326,""racyScore"":0.00951602},""TVisionCelebrities"":{""TOM CRUISE"":0.999997854},""_expires"":""2017-06-24T22:43:00.578981Z""},{""Emotion0"":{""anger"":1.77455458E-05,""contempt"":0.000173967157,""disgust"":0.000229260724,""fear"":3.15066657E-08,""happiness"":0.9786317,""neutral"":0.0209334176,""sadness"":5.861598E-07,""surprise"":1.33144977E-05},""_expires"":""2017-06-24T22:43:00.0793561Z""},{""Tags"":{""Connor Cruise"":1,""The Bible"":1,""Risky Business"":0.999,""Martinez"":0.006,""Complex"":0.01,""Twitter Inc."":1,""Sean Penn"":0.989,""Curtis Armstrong"":0.625,""The Hollywood Reporter"":0.203,""Louis Armstrong"":0.348,""Cruise ship"":1,""Chicago"":0.997,""Rebecca De Mornay"":0.434,""Christianity"":0.75,""James Corden"":0.069,""Hollywood"":1,""Stunt"":0.45},""_expires"":""2017-06-24T22:43:00.0008415Z""},{""XSentiment"":0.178008512,""_expires"":""2017-06-24T22:43:01.2750814Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/goldlink-releases-crew-remix-featuring-gucci-mane"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/goldlink-releases-crew-remix-featuring-gucci-mane""},""j"":[{""_title"":""Gucci Mane Jumps on Remix of GoldLink's \""Crew\""""},{""RVisionTags"":{""text"":0.9904163},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.140676036,""racyScore"":0.08041213},""_expires"":""2017-06-25T03:29:03.5393778Z""},{""Emotion0"":{""anger"":0.09716808,""contempt"":0.00540762255,""disgust"":0.0220363177,""fear"":0.0168951228,""happiness"":0.00362249953,""neutral"":0.772761941,""sadness"":0.028037779,""surprise"":0.05407063},""Emotion1"":{""anger"":0.419406265,""contempt"":0.032055337,""disgust"":0.03494472,""fear"":0.004095346,""happiness"":0.218424827,""neutral"":0.2713901,""sadness"":0.0143981427,""surprise"":0.005285231},""_expires"":""2017-06-25T03:29:01.7568819Z""},{""Tags"":{""Gucci Mane"":0.993,""Goldlink"":0.246,""Joshua"":0.003,""Twitter Inc."":0.987,""Public Relations"":0.054,""Washington, D.C."":1,""Shy Glizzy"":0.149,""Turntablism"":0.004,""Shazam"":0.915,""Apple Music"":0.473,""You Can"":0.003,""iTunes"":0.956,""United States"":1,""Country"":0.996,""Miami Heat"":0.888,""Houston Rockets"":0.81,""Los Angeles"":1,""Portland Trail Blazers"":0.972,""Brooklyn Nets"":0.74,""Philadelphia"":1,""Monument Records"":0.173,""Go-go"":0.558,""Down"":0.126,""Album"":0.984,""Xavier Musketeers men's basketball"":0.011,""TEAM*"":0.006,""No Way Out"":0.007},""_expires"":""2017-06-25T03:29:01.4924568Z""},{""XSentiment"":0.7643385,""_expires"":""2017-06-25T03:29:02.1167452Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/snoop-dogg-mock-young-thug-and-lil-uzi-vert-moment-i-feared-video"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/snoop-dogg-mock-young-thug-and-lil-uzi-vert-moment-i-feared-video""},""j"":[{""_title"":""It Looks Like Snoop Dogg Is Mocking Young Thug and Lil Uzi Vert in New Video for \""Moment I Feared\""""},{""RVisionTags"":{""person"":0.998487234},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0242747813,""racyScore"":0.02104937},""_expires"":""2017-06-24T20:58:27.8900749Z""},{""Emotion0"":{""anger"":0.000228447025,""contempt"":0.0007477614,""disgust"":0.000145930273,""fear"":1.614125E-07,""happiness"":0.00138306723,""neutral"":0.997234,""sadness"":0.000220693677,""surprise"":3.99426281E-05},""_expires"":""2017-06-24T20:58:27.5944467Z""},{""Tags"":{""Snoop Dogg"":1,""Young Thug"":0.959,""Uzi"":0.67,""Vert (music producer)"":0.003,""New Video"":0.003,""Kyle Broflovski"":0.032,""Philadelphia"":0.006,""Twitter Inc."":1,""Subscription business model"":0.008,""Complex"":0.995,""WorldStarHipHop"":0.237,""Rick Rock"":0.449,""Fonzie"":0.026,""Hyphy"":0.228,""YouTube"":0.999,""300 Entertainment"":0.009,""I Do"":0.006,""Billboard Music Award for Woman of the Year"":0.007,""You Know What It Is"":0.007,""Beautiful"":1,""Hip hop music"":1},""_expires"":""2017-06-24T20:58:27.1518034Z""},{""XSentiment"":0.0003323581,""_expires"":""2017-06-24T20:58:28.2026657Z""}]},{""_tag"":""cmplx$http://www.complex.com/sports/2017/06/will-lebron-james-ever-be-the-goat-square-up"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/sports/2017/06/will-lebron-james-ever-be-the-goat-square-up""},""j"":[{""_title"":""Square Up: Will LeBron James Ever Be the G.O.A.T.?""},{""RVisionTags"":{""person"":0.996526659,""outdoor"":0.8773945,""player"":0.8584581,""athletic game"":0.8081653,""sport"":0.7153003,""green"":0.640901864},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0165802147,""racyScore"":0.0130834375},""_expires"":""2017-06-24T17:04:57.3297536Z""},{""Emotion0"":{""anger"":0.03943785,""contempt"":0.05141066,""disgust"":0.0008398856,""fear"":3.766839E-05,""happiness"":0.00017841009,""neutral"":0.9013937,""sadness"":0.00586816063,""surprise"":0.0008336294},""_expires"":""2017-06-24T17:04:55.650471Z""},{""Tags"":{""Square, Inc."":0.005,""LeBron James"":1,""Complex"":0.926,""Twitter Inc."":0.991,""UNK NBA"":1,""Lil B"":0.415,""Cleveland Cavaliers"":1,""Golden State Warriors"":1},""_expires"":""2017-06-24T17:04:54.617989Z""},{""XSentiment"":0.00127977342,""_expires"":""2017-06-24T17:04:55.400445Z""}]},{""_tag"":""cmplx$http://www.complex.com/life/2017/06/cop-who-killed-philando-castile-says-smell-of-weed-made-him-fear-for-life"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/life/2017/06/cop-who-killed-philando-castile-says-smell-of-weed-made-him-fear-for-life""},""j"":[{""_title"":""Cop Who Killed Philando Castile Says Smell of Weed Made Him Fear for His Life""},{""RVisionTags"":{""sky"":0.998623848,""outdoor"":0.980991364},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.01796771,""racyScore"":0.0286844},""_expires"":""2017-06-24T16:33:36.9062355Z""},{""_expires"":""2017-06-24T16:33:36.2030935Z""},{""Tags"":{""Scared"":0.011,""Philadelphia"":0.693,""Twitter Inc."":1,""Ramsey County"":0.995,""County attorney"":0.084,""Minnesota"":1,""The Life of the Party"":0.009,""And I"":0.003,""Police"":0.238,""Dashcam"":0.092,""Murder"":0.673,""Complex"":0.508},""_expires"":""2017-06-24T16:33:35.8124592Z""},{""XSentiment"":4.440892E-16,""_expires"":""2017-06-24T16:33:36.4218429Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/a-history-of-bow-wow-fails"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/a-history-of-bow-wow-fails""},""j"":[{""_title"":""A History of Bow Wow Taking L's""},{""RVisionTags"":{""person"":0.999946833,""crowd"":0.236136854},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.02204849,""racyScore"":0.0223767031},""_expires"":""2017-06-24T15:56:22.5001293Z""},{""Emotion0"":{""anger"":1.22613847E-05,""contempt"":0.0200665444,""disgust"":0.000140431745,""fear"":8.001536E-09,""happiness"":0.883786,""neutral"":0.0959461853,""sadness"":1.50497419E-06,""surprise"":4.703166E-05},""_expires"":""2017-06-24T15:56:21.7295682Z""},{""Tags"":{""Bow Wow"":1,""He Is"":0.003,""Forbes"":0.995,""Hip hop music"":1,""iTunes"":0.974,""Atlantic Ocean"":0.051,""Vibe"":0.982,""The Source"":0.991,""GQ"":1,""Esquire"":0.998,""Stephen Sondheim"":0.606,""Twitter Inc."":1,""Snoop Dogg"":1,""The Arsenio Hall Show"":0.043,""Kurtis Blow"":0.325,""Michael Jordan"":0.785,""Vine"":0.585,""Oh, hell"":0.005,""Rent"":0.155,""Los Angeles"":1,""Grammy Awards"":1,""Scuderia Ferrari"":0.099,""Instagram"":0.996,""Migos"":0.01,""December 8"":0.003,""Live television"":0.401,""Grammy Awards Ceremony"":0.102,""Timothy Sykes"":0.173,""President of the United States"":0.998,""Donald Trump"":0.069,""Pacific Time Zone"":0.278,""Funkmaster Flex"":0.375,""Jermaine Dupri"":0.996,""Complex"":0.959},""_expires"":""2017-06-24T15:56:22.1957887Z""},{""XSentiment"":1,""_expires"":""2017-06-24T15:56:22.336416Z""}]},{""_tag"":""cmplx$http://www.complex.com/style/2017/06/how-rhude-one-of-best-los-angeles-brands-started-with-a-single-t-shirt"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/style/2017/06/how-rhude-one-of-best-los-angeles-brands-started-with-a-single-t-shirt""},""j"":[{""_title"":""How Rhude, One of the Best L.A. Brands, Started With a Single T-Shirt ""},{""RVisionTags"":{""person"":0.993622959,""floor"":0.989562333,""indoor"":0.98370254,""standing"":0.810940444,""curtain"":0.7955723,""suit"":0.335544765},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.08148283,""racyScore"":0.0423882678},""_expires"":""2017-06-24T12:04:37.9596756Z""},{""Emotion0"":{""anger"":0.0006152864,""contempt"":0.00599214761,""disgust"":0.0002703283,""fear"":1.61605785E-05,""happiness"":0.00174113235,""neutral"":0.984110057,""sadness"":0.006996317,""surprise"":0.0002585811},""Emotion1"":{""anger"":3.47017163E-07,""contempt"":0.00013010086,""disgust"":2.59245485E-06,""fear"":1.34914092E-06,""happiness"":0.000740572,""neutral"":0.997228861,""sadness"":0.00182852661,""surprise"":6.765087E-05},""_expires"":""2017-06-24T12:04:37.3269255Z""},{""Tags"":{""The Best"":0.013,""Starting pitcher"":0.489,""T-shirt"":0.041,""Twitter Inc."":0.955,""Mixmaster Spade"":0.977,""Kendrick Lamar"":1,""Snoop Dogg"":1,""BET Awards"":0.014,""ASAP Rocky"":0.979,""Kevin Durant"":0.023,""Jimmy Butler"":0.052,""Barneys New York"":0.011,""Patron saint"":0.034,""United States"":1,""Big Sean"":0.986,""Manila"":0.676,""Culture of the Philippines"":0.938,""Trade in Services Agreement"":0.006,""Kanye West"":1,""Arnold Schwarzenegger"":0.004,""Comme des Garçons"":0.987,""Rei Kawakubo"":0.911,""Sugar Land"":0.671,""Texas"":1,""Posttraumatic stress disorder"":0.02,""Earth, Wind & Fire"":0.003,""Mike Jones"":0.007,""White American"":0.012,""Prada Marfa"":1,""Elmgreen and Dragset"":0.992,""Complex"":0.992},""_expires"":""2017-06-24T12:04:36.8672077Z""},{""XSentiment"":1,""_expires"":""2017-06-24T12:04:37.5488193Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/bow-wow-gets-backlash-over-sexist-instagram-caption"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/bow-wow-gets-backlash-over-sexist-instagram-caption""},""j"":[{""_title"":""The Internet Blasts Bow Wow for His Message About Women's Behavior""},{""RVisionTags"":{""person"":0.962055564,""man"":0.9547968},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0112607479,""racyScore"":0.0106668333},""TVisionCelebrities"":{""Bow Wow"":0.9601661},""_expires"":""2017-06-24T01:43:32.5645689Z""},{""Emotion0"":{""anger"":0.0011651055,""contempt"":0.005131879,""disgust"":0.000264819653,""fear"":2.01475978E-05,""happiness"":0.000101240934,""neutral"":0.98257935,""sadness"":0.0105332062,""surprise"":0.000204226963},""_expires"":""2017-06-24T01:43:31.8143804Z""},{""Tags"":{""The Internet"":0.661,""Bow Wow"":0.398,""Twitter Inc."":1,""Instagram"":0.998,""I Wonder Why"":0.004,""Sexism"":0.362},""_expires"":""2017-06-24T01:43:31.5472109Z""},{""XSentiment"":0.99999994,""_expires"":""2017-06-24T01:43:32.2439044Z""}]}]},""p"":[0.8153846,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154],""VWState"":{""m"":""4c5bcf8eb1ef4d3ba327dacba2f336a1/4c5bcf8eb1ef4d3ba327dacba2f336a1""}}"; + var json = @"{""Version"":""1"",""EventId"":""7cacacea2c6e49b5b922f6f517a325ed"",""a"":[9,4,13,10,8,5,2,3,12,11,7,6,1],""c"":{""_synthetic"":false,""User"":{""_age"":0},""Geo"":{""country"":""United States"",""_countrycf"":""8"",""state"":""Georgia"",""city"":""Stone Mountain"",""_citycf"":""5"",""dma"":""524""},""MRefer"":{""referer"":""http://www.complex.com/""},""OUserAgent"":{""_ua"":""Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4"",""_DeviceBrand"":"""",""_DeviceFamily"":""Other"",""_DeviceIsSpider"":false,""_DeviceModel"":"""",""_OSFamily"":""Mac OS X"",""_OSMajor"":""10"",""_OSPatch"":""5"",""DeviceType"":""Desktop""},""_multi"":[{""_tag"":""cmplx$http://www.complex.com/music/2017/06/prodigy-mobb-deep-once-in-a-generation-rapper"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/prodigy-mobb-deep-once-in-a-generation-rapper""},""j"":[{""_title"":""Why Prodigy Was A Once-In-A-Generation Rapper""},{""RVisionTags"":{""person"":0.9913805,""hat"":0.6433856,""male"":0.153918922},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0162594952,""racyScore"":0.0152094},""TVisionCelebrities"":{""Prodigy"":0.9999119},""_expires"":""2017-06-24T22:43:00.9241929Z""},{""Emotion0"":{""anger"":0.005261584,""contempt"":0.01940289,""disgust"":0.00146069494,""fear"":7.486289E-05,""happiness"":0.0102698216,""neutral"":0.9544214,""sadness"":0.00859182,""surprise"":0.00051694276},""_expires"":""2017-06-24T22:43:00.0008415Z""},{""Tags"":{""Roc Marciano"":0.015,""Mobb Deep"":1,""Prodigy"":1,""Havoc"":1,""A Tribe Called Quest"":0.969,""Q-Tip"":0.992,""The Infamous"":1,""The Crystals"":0.01,""Fifth Beatle"":0.099},""_expires"":""2017-06-24T22:43:00.1727355Z""},{""XSentiment"":3.01036973E-13,""_expires"":""2017-06-24T22:43:00.9398236Z""}]},{""_tag"":""cmplx$http://www.complex.com/pop-culture/2017/06/best-movies-2017"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/pop-culture/2017/06/best-movies-2017""},""j"":[{""_title"":""The Best Movies of 2017 (So Far)""},{""RVisionTags"":{""text"":0.9992092,""book"":0.997986436},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0431842171,""racyScore"":0.0587232448},""_expires"":""2017-06-25T03:08:01.2111373Z""},{""Emotion0"":{""anger"":0.005728849,""contempt"":0.00117533945,""disgust"":8.215821E-05,""fear"":1.32827354E-05,""happiness"":0.000487715733,""neutral"":0.9911194,""sadness"":0.000120451317,""surprise"":0.00127282448},""_expires"":""2017-06-25T03:08:00.8636383Z""},null,{""XSentiment"":1,""_expires"":""2017-06-25T03:08:01.5112947Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak""},""j"":[{""_title"":""Rihanna Responds to DM From Fan Seeking Advice on Getting Over His First Heartbreak""},{""RVisionTags"":{""person"":0.99633044},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.013516671,""racyScore"":0.048148632},""_expires"":""2017-06-25T02:01:20.2363494Z""},{""Emotion0"":{""anger"":3.085194E-06,""contempt"":0.000411317043,""disgust"":1.026042E-05,""fear"":3.766979E-07,""happiness"":0.9783716,""neutral"":0.0211082119,""sadness"":5.66137569E-05,""surprise"":3.850023E-05},""_expires"":""2017-06-25T02:01:19.7050726Z""},{""Tags"":{""Rihanna"":1,""Martinez"":0.021,""Complex"":0.834,""Twitter Inc."":1,""If You"":0.003,""Grammy Awards"":1,""Mathematics"":0.999,""Malawi"":0.298,""Christine Teigen"":0.009,""Dave Chappelle"":0.047,""Presenter"":0.892,""Kendrick Lamar"":0.984,""Diamonds"":0.998},""_expires"":""2017-06-25T02:01:19.4155039Z""},{""XSentiment"":1,""_expires"":""2017-06-25T02:01:24.9467431Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/rihanna-responds-to-dm-from-fan-seeking-advice-on-getting-over-his-first-heartbreak""},""j"":[{""_title"":""Rihanna Responds to DM From Fan Seeking Advice on Getting Over His First Heartbreak""}]},{""_tag"":""cmplx$http://www.complex.com/life/2017/06/guy-changes-from-shorts-to-dress-after-getting-sent-home-from-work-on-hot-day"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/life/2017/06/guy-changes-from-shorts-to-dress-after-getting-sent-home-from-work-on-hot-day""},""j"":[{""_title"":""Guy Sent Home by Boss for Wearing Shorts on a Hot Day, Returns to Work in Mom's Dress""},{""RVisionTags"":{""person"":0.999545038,""indoor"":0.998509467,""wall"":0.997952163},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.034187492,""racyScore"":0.0335518233},""_expires"":""2017-06-24T21:27:46.7135793Z""},{""Emotion0"":{""anger"":7.7880286E-05,""contempt"":0.00116215891,""disgust"":4.65850326E-06,""fear"":3.797253E-06,""happiness"":2.82076635E-05,""neutral"":0.986993253,""sadness"":0.0116343917,""surprise"":9.564323E-05},""_expires"":""2017-06-24T21:27:46.1197986Z""},{""Tags"":{""Boss Corporation"":0.007,""Shorts"":0.139,""Complex"":0.169,""Twitter Inc."":1,""English"":0.603,""The Daily Mirror"":0.008,""Oklahoma"":0.948,""High school"":0.999,""Lists of National Basketball Association players"":0.072,""UNK NBA"":0.529,""NBA dress code"":0.031,""Dress code"":0.113},""_expires"":""2017-06-24T21:27:46.0729078Z""},{""XSentiment"":0.9995655,""_expires"":""2017-06-24T21:27:46.7448359Z""}]},{""_tag"":""cmplx$http://www.complex.com/pop-culture/2017/06/tom-cruise-allegedly-balanced-bible-study-and-blow-jobs-away-from-risky-business-set"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/pop-culture/2017/06/tom-cruise-allegedly-balanced-bible-study-and-blow-jobs-away-from-risky-business-set""},""j"":[{""_title"":""Tom Cruise Was Allegedly Balancing Bible Study and Blow Jobs Away From the 'Risky Business' Set""},{""RVisionTags"":{""person"":0.9992724,""man"":0.9368908},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.009141326,""racyScore"":0.00951602},""TVisionCelebrities"":{""TOM CRUISE"":0.999997854},""_expires"":""2017-06-24T22:43:00.578981Z""},{""Emotion0"":{""anger"":1.77455458E-05,""contempt"":0.000173967157,""disgust"":0.000229260724,""fear"":3.15066657E-08,""happiness"":0.9786317,""neutral"":0.0209334176,""sadness"":5.861598E-07,""surprise"":1.33144977E-05},""_expires"":""2017-06-24T22:43:00.0793561Z""},{""Tags"":{""Connor Cruise"":1,""The Bible"":1,""Risky Business"":0.999,""Martinez"":0.006,""Complex"":0.01,""Twitter Inc."":1,""Sean Penn"":0.989,""Curtis Armstrong"":0.625,""The Hollywood Reporter"":0.203,""Louis Armstrong"":0.348,""Cruise ship"":1,""Chicago"":0.997,""Rebecca De Mornay"":0.434,""Christianity"":0.75,""James Corden"":0.069,""Hollywood"":1,""Stunt"":0.45},""_expires"":""2017-06-24T22:43:00.0008415Z""},{""XSentiment"":0.178008512,""_expires"":""2017-06-24T22:43:01.2750814Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/goldlink-releases-crew-remix-featuring-gucci-mane"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/goldlink-releases-crew-remix-featuring-gucci-mane""},""j"":[{""_title"":""Gucci Mane Jumps on Remix of GoldLink's \""Crew\""""},{""RVisionTags"":{""text"":0.9904163},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.140676036,""racyScore"":0.08041213},""_expires"":""2017-06-25T03:29:03.5393778Z""},{""Emotion0"":{""anger"":0.09716808,""contempt"":0.00540762255,""disgust"":0.0220363177,""fear"":0.0168951228,""happiness"":0.00362249953,""neutral"":0.772761941,""sadness"":0.028037779,""surprise"":0.05407063},""Emotion1"":{""anger"":0.419406265,""contempt"":0.032055337,""disgust"":0.03494472,""fear"":0.004095346,""happiness"":0.218424827,""neutral"":0.2713901,""sadness"":0.0143981427,""surprise"":0.005285231},""_expires"":""2017-06-25T03:29:01.7568819Z""},{""Tags"":{""Gucci Mane"":0.993,""Goldlink"":0.246,""Joshua"":0.003,""Twitter Inc."":0.987,""Public Relations"":0.054,""Washington, D.C."":1,""Shy Glizzy"":0.149,""Turntablism"":0.004,""Shazam"":0.915,""Apple Music"":0.473,""You Can"":0.003,""iTunes"":0.956,""United States"":1,""Country"":0.996,""Miami Heat"":0.888,""Houston Rockets"":0.81,""Los Angeles"":1,""Portland Trail Blazers"":0.972,""Brooklyn Nets"":0.74,""Philadelphia"":1,""Monument Records"":0.173,""Go-go"":0.558,""Down"":0.126,""Album"":0.984,""Xavier Musketeers men's basketball"":0.011,""TEAM*"":0.006,""No Way Out"":0.007},""_expires"":""2017-06-25T03:29:01.4924568Z""},{""XSentiment"":0.7643385,""_expires"":""2017-06-25T03:29:02.1167452Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/snoop-dogg-mock-young-thug-and-lil-uzi-vert-moment-i-feared-video"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/snoop-dogg-mock-young-thug-and-lil-uzi-vert-moment-i-feared-video""},""j"":[{""_title"":""It Looks Like Snoop Dogg Is Mocking Young Thug and Lil Uzi Vert in New Video for \""Moment I Feared\""""},{""RVisionTags"":{""person"":0.998487234},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0242747813,""racyScore"":0.02104937},""_expires"":""2017-06-24T20:58:27.8900749Z""},{""Emotion0"":{""anger"":0.000228447025,""contempt"":0.0007477614,""disgust"":0.000145930273,""fear"":1.614125E-07,""happiness"":0.00138306723,""neutral"":0.997234,""sadness"":0.000220693677,""surprise"":3.99426281E-05},""_expires"":""2017-06-24T20:58:27.5944467Z""},{""Tags"":{""Snoop Dogg"":1,""Young Thug"":0.959,""Uzi"":0.67,""Vert (music producer)"":0.003,""New Video"":0.003,""Kyle Broflovski"":0.032,""Philadelphia"":0.006,""Twitter Inc."":1,""Subscription business model"":0.008,""Complex"":0.995,""WorldStarHipHop"":0.237,""Rick Rock"":0.449,""Fonzie"":0.026,""Hyphy"":0.228,""YouTube"":0.999,""300 Entertainment"":0.009,""I Do"":0.006,""Billboard Music Award for Woman of the Year"":0.007,""You Know What It Is"":0.007,""Beautiful"":1,""Hip hop music"":1},""_expires"":""2017-06-24T20:58:27.1518034Z""},{""XSentiment"":0.0003323581,""_expires"":""2017-06-24T20:58:28.2026657Z""}]},{""_tag"":""cmplx$http://www.complex.com/sports/2017/06/will-lebron-james-ever-be-the-goat-square-up"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/sports/2017/06/will-lebron-james-ever-be-the-goat-square-up""},""j"":[{""_title"":""Square Up: Will LeBron James Ever Be the G.O.A.T.?""},{""RVisionTags"":{""person"":0.996526659,""outdoor"":0.8773945,""player"":0.8584581,""athletic game"":0.8081653,""sport"":0.7153003,""green"":0.640901864},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0165802147,""racyScore"":0.0130834375},""_expires"":""2017-06-24T17:04:57.3297536Z""},{""Emotion0"":{""anger"":0.03943785,""contempt"":0.05141066,""disgust"":0.0008398856,""fear"":3.766839E-05,""happiness"":0.00017841009,""neutral"":0.9013937,""sadness"":0.00586816063,""surprise"":0.0008336294},""_expires"":""2017-06-24T17:04:55.650471Z""},{""Tags"":{""Square, Inc."":0.005,""LeBron James"":1,""Complex"":0.926,""Twitter Inc."":0.991,""UNK NBA"":1,""Lil B"":0.415,""Cleveland Cavaliers"":1,""Golden State Warriors"":1},""_expires"":""2017-06-24T17:04:54.617989Z""},{""XSentiment"":0.00127977342,""_expires"":""2017-06-24T17:04:55.400445Z""}]},{""_tag"":""cmplx$http://www.complex.com/life/2017/06/cop-who-killed-philando-castile-says-smell-of-weed-made-him-fear-for-life"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/life/2017/06/cop-who-killed-philando-castile-says-smell-of-weed-made-him-fear-for-life""},""j"":[{""_title"":""Cop Who Killed Philando Castile Says Smell of Weed Made Him Fear for His Life""},{""RVisionTags"":{""sky"":0.998623848,""outdoor"":0.980991364},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.01796771,""racyScore"":0.0286844},""_expires"":""2017-06-24T16:33:36.9062355Z""},{""_expires"":""2017-06-24T16:33:36.2030935Z""},{""Tags"":{""Scared"":0.011,""Philadelphia"":0.693,""Twitter Inc."":1,""Ramsey County"":0.995,""County attorney"":0.084,""Minnesota"":1,""The Life of the Party"":0.009,""And I"":0.003,""Police"":0.238,""Dashcam"":0.092,""Murder"":0.673,""Complex"":0.508},""_expires"":""2017-06-24T16:33:35.8124592Z""},{""XSentiment"":4.440892E-16,""_expires"":""2017-06-24T16:33:36.4218429Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/a-history-of-bow-wow-fails"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/a-history-of-bow-wow-fails""},""j"":[{""_title"":""A History of Bow Wow Taking L's""},{""RVisionTags"":{""person"":0.999946833,""crowd"":0.236136854},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.02204849,""racyScore"":0.0223767031},""_expires"":""2017-06-24T15:56:22.5001293Z""},{""Emotion0"":{""anger"":1.22613847E-05,""contempt"":0.0200665444,""disgust"":0.000140431745,""fear"":8.001536E-09,""happiness"":0.883786,""neutral"":0.0959461853,""sadness"":1.50497419E-06,""surprise"":4.703166E-05},""_expires"":""2017-06-24T15:56:21.7295682Z""},{""Tags"":{""Bow Wow"":1,""He Is"":0.003,""Forbes"":0.995,""Hip hop music"":1,""iTunes"":0.974,""Atlantic Ocean"":0.051,""Vibe"":0.982,""The Source"":0.991,""GQ"":1,""Esquire"":0.998,""Stephen Sondheim"":0.606,""Twitter Inc."":1,""Snoop Dogg"":1,""The Arsenio Hall Show"":0.043,""Kurtis Blow"":0.325,""Michael Jordan"":0.785,""Vine"":0.585,""Oh, hell"":0.005,""Rent"":0.155,""Los Angeles"":1,""Grammy Awards"":1,""Scuderia Ferrari"":0.099,""Instagram"":0.996,""Migos"":0.01,""December 8"":0.003,""Live television"":0.401,""Grammy Awards Ceremony"":0.102,""Timothy Sykes"":0.173,""President of the United States"":0.998,""Donald Trump"":0.069,""Pacific Time Zone"":0.278,""Funkmaster Flex"":0.375,""Jermaine Dupri"":0.996,""Complex"":0.959},""_expires"":""2017-06-24T15:56:22.1957887Z""},{""XSentiment"":1,""_expires"":""2017-06-24T15:56:22.336416Z""}]},{""_tag"":""cmplx$http://www.complex.com/style/2017/06/how-rhude-one-of-best-los-angeles-brands-started-with-a-single-t-shirt"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/style/2017/06/how-rhude-one-of-best-los-angeles-brands-started-with-a-single-t-shirt""},""j"":[{""_title"":""How Rhude, One of the Best L.A. Brands, Started With a Single T-Shirt ""},{""RVisionTags"":{""person"":0.993622959,""floor"":0.989562333,""indoor"":0.98370254,""standing"":0.810940444,""curtain"":0.7955723,""suit"":0.335544765},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.08148283,""racyScore"":0.0423882678},""_expires"":""2017-06-24T12:04:37.9596756Z""},{""Emotion0"":{""anger"":0.0006152864,""contempt"":0.00599214761,""disgust"":0.0002703283,""fear"":1.61605785E-05,""happiness"":0.00174113235,""neutral"":0.984110057,""sadness"":0.006996317,""surprise"":0.0002585811},""Emotion1"":{""anger"":3.47017163E-07,""contempt"":0.00013010086,""disgust"":2.59245485E-06,""fear"":1.34914092E-06,""happiness"":0.000740572,""neutral"":0.997228861,""sadness"":0.00182852661,""surprise"":6.765087E-05},""_expires"":""2017-06-24T12:04:37.3269255Z""},{""Tags"":{""The Best"":0.013,""Starting pitcher"":0.489,""T-shirt"":0.041,""Twitter Inc."":0.955,""Mixmaster Spade"":0.977,""Kendrick Lamar"":1,""Snoop Dogg"":1,""BET Awards"":0.014,""ASAP Rocky"":0.979,""Kevin Durant"":0.023,""Jimmy Butler"":0.052,""Barneys New York"":0.011,""Patron saint"":0.034,""United States"":1,""Big Sean"":0.986,""Manila"":0.676,""Culture of the Philippines"":0.938,""Trade in Services Agreement"":0.006,""Kanye West"":1,""Arnold Schwarzenegger"":0.004,""Comme des Garçons"":0.987,""Rei Kawakubo"":0.911,""Sugar Land"":0.671,""Texas"":1,""Posttraumatic stress disorder"":0.02,""Earth, Wind & Fire"":0.003,""Mike Jones"":0.007,""White American"":0.012,""Prada Marfa"":1,""Elmgreen and Dragset"":0.992,""Complex"":0.992},""_expires"":""2017-06-24T12:04:36.8672077Z""},{""XSentiment"":1,""_expires"":""2017-06-24T12:04:37.5488193Z""}]},{""_tag"":""cmplx$http://www.complex.com/music/2017/06/bow-wow-gets-backlash-over-sexist-instagram-caption"",""i"":{""constant"":1,""id"":""cmplx$http://www.complex.com/music/2017/06/bow-wow-gets-backlash-over-sexist-instagram-caption""},""j"":[{""_title"":""The Internet Blasts Bow Wow for His Message About Women's Behavior""},{""RVisionTags"":{""person"":0.962055564,""man"":0.9547968},""SVisionAdult"":{""isAdultContent"":false,""isRacyContent"":false,""adultScore"":0.0112607479,""racyScore"":0.0106668333},""TVisionCelebrities"":{""Bow Wow"":0.9601661},""_expires"":""2017-06-24T01:43:32.5645689Z""},{""Emotion0"":{""anger"":0.0011651055,""contempt"":0.005131879,""disgust"":0.000264819653,""fear"":2.01475978E-05,""happiness"":0.000101240934,""neutral"":0.98257935,""sadness"":0.0105332062,""surprise"":0.000204226963},""_expires"":""2017-06-24T01:43:31.8143804Z""},{""Tags"":{""The Internet"":0.661,""Bow Wow"":0.398,""Twitter Inc."":1,""Instagram"":0.998,""I Wonder Why"":0.004,""Sexism"":0.362},""_expires"":""2017-06-24T01:43:31.5472109Z""},{""XSentiment"":0.99999994,""_expires"":""2017-06-24T01:43:32.2439044Z""}]}]},""p"":[0.8153846,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154,0.0153846154],""VWState"":{""m"":""4c5bcf8eb1ef4d3ba327dacba2f336a1/4c5bcf8eb1ef4d3ba327dacba2f336a1""}}"; - using (var vw = new VowpalWabbit("--cb_adf")) + using (var vw = new VowpalWabbit("--cb_adf --dsjson")) { var obj = JsonConvert.DeserializeObject(json); var bytes = new byte[Encoding.UTF8.GetMaxByteCount(json.Length) + 1]; diff --git a/test/unit_test/CMakeLists.txt b/test/unit_test/CMakeLists.txt index 375cc21a7ad..b8f606e612c 100644 --- a/test/unit_test/CMakeLists.txt +++ b/test/unit_test/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable(vw-unit-test.out main.cc options_test.cc options_boost_po_test.cc cb_explore_adf_test.cc explore_test.cc - stable_unique_tests.cc test_common.h object_pool_test.cc + stable_unique_tests.cc test_common.h object_pool_test.cc ccb_parser_test.cc json_parser_test.cc + dsjson_parser_test.cc ) # Add the include directories from vw target for testing diff --git a/test/unit_test/ccb_parser_test.cc b/test/unit_test/ccb_parser_test.cc new file mode 100644 index 00000000000..1eb916ac4dd --- /dev/null +++ b/test/unit_test/ccb_parser_test.cc @@ -0,0 +1,143 @@ +#define BOOST_TEST_DYN_LINK + +#include +#include + +#include "test_common.h" + +#include +#include "conditional_contextual_bandit.h" +#include "parser.h" + +CCB::label parse_label(parser* p, std::string label) +{ + auto lp = CCB::ccb_label_parser; + tokenize(' ', { const_cast(label.c_str()), const_cast(label.c_str()) + strlen(label.c_str()) }, p->words); + CCB::label l; + lp.default_label(&l); + lp.parse_label(p, nullptr, &l, p->words); + + return l; +} + +BOOST_AUTO_TEST_CASE(ccb_parse_label) +{ + parser p; + p.words = v_init(); + p.parse_name = v_init(); + + auto label = parse_label(&p, "ccb shared"); + BOOST_CHECK_EQUAL(label.explicit_included_actions.size(), 0); + BOOST_CHECK(label.outcome == nullptr); + BOOST_CHECK_EQUAL(label.type, CCB::example_type::shared); + + label = parse_label(&p, "ccb action"); + BOOST_CHECK_EQUAL(label.explicit_included_actions.size(), 0); + BOOST_CHECK(label.outcome == nullptr); + BOOST_CHECK_EQUAL(label.type, CCB::example_type::action); + + label = parse_label(&p, "ccb decision"); + BOOST_CHECK_EQUAL(label.explicit_included_actions.size(), 0); + BOOST_CHECK(label.outcome == nullptr); + BOOST_CHECK_EQUAL(label.type, CCB::example_type::decision); + + label = parse_label(&p, "ccb decision 1,3,4"); + BOOST_CHECK_EQUAL(label.explicit_included_actions.size(), 3); + BOOST_CHECK_EQUAL(label.explicit_included_actions[0], 1); + BOOST_CHECK_EQUAL(label.explicit_included_actions[1], 3); + BOOST_CHECK_EQUAL(label.explicit_included_actions[2], 4); + BOOST_CHECK(label.outcome == nullptr); + BOOST_CHECK_EQUAL(label.type, CCB::example_type::decision); + + label = parse_label(&p, "ccb decision 1:1.0:0.5 3"); + BOOST_CHECK_EQUAL(label.explicit_included_actions.size(), 1); + BOOST_CHECK_EQUAL(label.explicit_included_actions[0], 3); + BOOST_CHECK_CLOSE(label.outcome->cost, 1.0f, FLOAT_TOL); + BOOST_CHECK_EQUAL(label.outcome->probabilities.size(), 1); + BOOST_CHECK_EQUAL(label.outcome->probabilities[0].action, 1); + BOOST_CHECK_CLOSE(label.outcome->probabilities[0].score, .5f, FLOAT_TOL); + BOOST_CHECK_EQUAL(label.type, CCB::example_type::decision); + + label = parse_label(&p, "ccb decision 1:-2.0:0.5,2:0.25,3:0.25 3,4"); + BOOST_CHECK_EQUAL(label.explicit_included_actions.size(), 2); + BOOST_CHECK_EQUAL(label.explicit_included_actions[0], 3); + BOOST_CHECK_EQUAL(label.explicit_included_actions[1], 4); + BOOST_CHECK_CLOSE(label.outcome->cost, -2.0f, FLOAT_TOL); + BOOST_CHECK_EQUAL(label.outcome->probabilities.size(), 3); + BOOST_CHECK_EQUAL(label.outcome->probabilities[0].action, 1); + BOOST_CHECK_CLOSE(label.outcome->probabilities[0].score, .5f, FLOAT_TOL); + BOOST_CHECK_EQUAL(label.outcome->probabilities[1].action, 2); + BOOST_CHECK_CLOSE(label.outcome->probabilities[1].score, .25f, FLOAT_TOL); + BOOST_CHECK_EQUAL(label.outcome->probabilities[2].action, 3); + BOOST_CHECK_CLOSE(label.outcome->probabilities[2].score, .25f, FLOAT_TOL); + BOOST_CHECK_EQUAL(label.type, CCB::example_type::decision); + + BOOST_REQUIRE_THROW(parse_label(&p, "shared"), VW::vw_exception); + BOOST_REQUIRE_THROW(parse_label(&p, "other shared"), VW::vw_exception); + BOOST_REQUIRE_THROW(parse_label(&p, "other"), VW::vw_exception); + BOOST_REQUIRE_THROW(parse_label(&p, "ccb unknown"), VW::vw_exception); + BOOST_REQUIRE_THROW(parse_label(&p, "ccb decision 1:1.0:0.5,4:0.7"), VW::vw_exception); +} + +BOOST_AUTO_TEST_CASE(ccb_cache_label) +{ + io_buf io; + io.init(); + io.space.resize(1000); + io.space.end() = io.space.begin() + 1000; + + parser p; + p.words = v_init(); + p.parse_name = v_init(); + + auto lp = CCB::ccb_label_parser; + auto label = parse_label(&p, "ccb decision 1:-2.0:0.5,2:0.25,3:0.25 3,4"); + + lp.cache_label(&label, io); + io.head = io.space.begin(); + + CCB::label uncached_label; + lp.default_label(&uncached_label); + lp.read_cached_label(nullptr, &uncached_label, io); + + BOOST_CHECK_EQUAL(uncached_label.explicit_included_actions.size(), 2); + BOOST_CHECK_EQUAL(uncached_label.explicit_included_actions[0], 3); + BOOST_CHECK_EQUAL(uncached_label.explicit_included_actions[1], 4); + BOOST_CHECK_CLOSE(uncached_label.outcome->cost, -2.0f, FLOAT_TOL); + BOOST_CHECK_EQUAL(uncached_label.outcome->probabilities.size(), 3); + BOOST_CHECK_EQUAL(uncached_label.outcome->probabilities[0].action, 1); + BOOST_CHECK_CLOSE(uncached_label.outcome->probabilities[0].score, .5f, FLOAT_TOL); + BOOST_CHECK_EQUAL(uncached_label.outcome->probabilities[1].action, 2); + BOOST_CHECK_CLOSE(uncached_label.outcome->probabilities[1].score, .25f, FLOAT_TOL); + BOOST_CHECK_EQUAL(uncached_label.outcome->probabilities[2].action, 3); + BOOST_CHECK_CLOSE(uncached_label.outcome->probabilities[2].score, .25f, FLOAT_TOL); + BOOST_CHECK_EQUAL(uncached_label.type, CCB::example_type::decision); +} + +BOOST_AUTO_TEST_CASE(ccb_copy_label) +{ + parser p; + p.words = v_init(); + p.parse_name = v_init(); + auto lp = CCB::ccb_label_parser; + + auto label = parse_label(&p, "ccb decision 1:-2.0:0.5,2:0.25,3:0.25 3,4"); + + CCB::label copied_to; + lp.default_label(&copied_to); + + lp.copy_label(&copied_to, &label); + + BOOST_CHECK_EQUAL(copied_to.explicit_included_actions.size(), 2); + BOOST_CHECK_EQUAL(copied_to.explicit_included_actions[0], 3); + BOOST_CHECK_EQUAL(copied_to.explicit_included_actions[1], 4); + BOOST_CHECK_CLOSE(copied_to.outcome->cost, -2.0f, FLOAT_TOL); + BOOST_CHECK_EQUAL(copied_to.outcome->probabilities.size(), 3); + BOOST_CHECK_EQUAL(copied_to.outcome->probabilities[0].action, 1); + BOOST_CHECK_CLOSE(copied_to.outcome->probabilities[0].score, .5f, FLOAT_TOL); + BOOST_CHECK_EQUAL(copied_to.outcome->probabilities[1].action, 2); + BOOST_CHECK_CLOSE(copied_to.outcome->probabilities[1].score, .25f, FLOAT_TOL); + BOOST_CHECK_EQUAL(copied_to.outcome->probabilities[2].action, 3); + BOOST_CHECK_CLOSE(copied_to.outcome->probabilities[2].score, .25f, FLOAT_TOL); + BOOST_CHECK_EQUAL(copied_to.type, CCB::example_type::decision); +} diff --git a/test/unit_test/dsjson_parser_test.cc b/test/unit_test/dsjson_parser_test.cc new file mode 100644 index 00000000000..22938926a3c --- /dev/null +++ b/test/unit_test/dsjson_parser_test.cc @@ -0,0 +1,186 @@ +#define BOOST_TEST_DYN_LINK + +#include +#include + +#include "test_common.h" + +#include +#include "conditional_contextual_bandit.h" +#include "parse_example_json.h" + +v_array parse_dsjson(vw& all, std::string line) +{ + auto examples = v_init(); + examples.push_back(&VW::get_unused_example(&all)); + DecisionServiceInteraction interaction; + + VW::read_line_decision_service_json(all, examples, (char*)line.c_str(), line.size(), false, + (VW::example_factory_t)&VW::get_unused_example, (void*)&all, &interaction); + + return examples; +} + +// TODO: Make unit test dig out and verify features. +BOOST_AUTO_TEST_CASE(parse_dsjson_cb) +{ + std::string json_text = R"( +{ + "_label_cost": -1, + "_label_probability": 0.8166667, + "_label_Action": 2, + "_labelIndex": 1, + "Version": "1", + "EventId": "0074434d3a3a46529f65de8a59631939", + "a": [ + 2, + 1, + 3 + ], + "c": { + "shared_ns": { + "shared_feature": 0 + }, + "_multi": [ + { + "_tag": "tag", + "ns1": { + "f1": 1, + "f2": "strng" + }, + "ns2": [ + { + "f3": "value1" + }, + { + "ns3": { + "f4": 0.994963765 + } + } + ] + }, + { + "_tag": "tag", + "ns1": { + "f1": 1, + "f2": "strng" + } + }, + { + "_tag": "tag", + "ns1": { + "f1": 1, + "f2": "strng" + } + } + ] + }, + "p": [ + 0.816666663, + 0.183333333, + 0.183333333 + ], + "VWState": { + "m": "096200c6c41e42bbb879c12830247637/0639c12bea464192828b250ffc389657" + } +} +)"; + auto vw = VW::initialize("--dsjson --cb_adf --no_stdin", nullptr, false, nullptr, nullptr); + auto examples = parse_dsjson(*vw, json_text); + + BOOST_CHECK_EQUAL(examples.size(), 4); + + // Shared example + BOOST_CHECK_EQUAL(examples[0]->l.cb.costs.size(), 1); + BOOST_CHECK_CLOSE(examples[0]->l.cb.costs[0].probability, -1.f, FLOAT_TOL); + BOOST_CHECK_CLOSE(examples[0]->l.cb.costs[0].cost, FLT_MAX, FLOAT_TOL); + + // Action examples + BOOST_CHECK_EQUAL(examples[1]->l.cb.costs.size(), 0); + BOOST_CHECK_EQUAL(examples[2]->l.cb.costs.size(), 1); + BOOST_CHECK_EQUAL(examples[3]->l.cb.costs.size(), 0); + + BOOST_CHECK_CLOSE(examples[2]->l.cb.costs[0].probability, 0.8166667, FLOAT_TOL); + BOOST_CHECK_CLOSE(examples[2]->l.cb.costs[0].cost, -1.0, FLOAT_TOL); + BOOST_CHECK_EQUAL(examples[2]->l.cb.costs[0].action, 2); +} + +// TODO: Make unit test dig out and verify features. +BOOST_AUTO_TEST_CASE(parse_dsjson_ccb) +{ + std::string json_text = R"( +{ + "Timestamp":"timestamp_utc", + "Version": "1", + "c":{ + "_multi": [ + { + "b_": "1", + "c_": "1", + "d_": "1" + }, + { + "b_": "2", + "c_": "2", + "d_": "2" + } + ], + "_df":[ + { + "_id": "00eef1eb-2205-4f47", + "_inc": [1,2], + "test": 4 + }, + { + "_id": "set_id", + "other": 6 + } + ] + }, + "_decisions":[{ + "_label_cost": 2, + "_o": [], + "_a": 1, + "_p": 0.25 + }, + { + "_label_cost": 4, + "_o":[], + "_a": [2, 1], + "_p": [0.75, 0.25] + } + ], + "VWState": { + "m": "096200c6c41e42bbb879c12830247637/0639c12bea464192828b250ffc389657" + } +} +)"; + + auto vw = VW::initialize("--ccb_explore_adf --dsjson --no_stdin", nullptr, false, nullptr, nullptr); + auto examples = parse_dsjson(*vw, json_text); + + BOOST_CHECK_EQUAL(examples.size(), 5); + BOOST_CHECK_EQUAL(examples[0]->l.conditional_contextual_bandit.type, CCB::example_type::shared); + BOOST_CHECK_EQUAL(examples[1]->l.conditional_contextual_bandit.type, CCB::example_type::action); + BOOST_CHECK_EQUAL(examples[2]->l.conditional_contextual_bandit.type, CCB::example_type::action); + BOOST_CHECK_EQUAL(examples[3]->l.conditional_contextual_bandit.type, CCB::example_type::decision); + BOOST_CHECK_EQUAL(examples[4]->l.conditional_contextual_bandit.type, CCB::example_type::decision); + + auto label1 = examples[3]->l.conditional_contextual_bandit; + BOOST_CHECK_EQUAL(label1.explicit_included_actions.size(), 2); + BOOST_CHECK_EQUAL(label1.explicit_included_actions[0], 1); + BOOST_CHECK_EQUAL(label1.explicit_included_actions[1], 2); + BOOST_CHECK_CLOSE(label1.outcome->cost, 2.f, .0001f); + BOOST_CHECK_EQUAL(label1.outcome->probabilities.size(), 1); + BOOST_CHECK_EQUAL(label1.outcome->probabilities[0].action, 1); + BOOST_CHECK_CLOSE(label1.outcome->probabilities[0].score, .25f, .0001f); + + auto label2 = examples[4]->l.conditional_contextual_bandit; + BOOST_CHECK_EQUAL(label2.explicit_included_actions.size(), 0); + BOOST_CHECK_CLOSE(label2.outcome->cost, 4.f, .0001f); + BOOST_CHECK_EQUAL(label2.outcome->probabilities.size(), 2); + BOOST_CHECK_EQUAL(label2.outcome->probabilities[0].action, 2); + BOOST_CHECK_CLOSE(label2.outcome->probabilities[0].score, .75f, .0001f); + BOOST_CHECK_EQUAL(label2.outcome->probabilities[1].action, 1); + BOOST_CHECK_CLOSE(label2.outcome->probabilities[1].score, .25f, .0001f); +} diff --git a/test/unit_test/json_parser_test.cc b/test/unit_test/json_parser_test.cc new file mode 100644 index 00000000000..8cd1adac70b --- /dev/null +++ b/test/unit_test/json_parser_test.cc @@ -0,0 +1,178 @@ +#define BOOST_TEST_DYN_LINK + +#include +#include + +#include "test_common.h" + +#include +#include "conditional_contextual_bandit.h" +#include "parse_example_json.h" + +v_array parse_json(vw& all, std::string line) +{ + auto examples = v_init(); + examples.push_back(&VW::get_unused_example(&all)); + VW::read_line_json( + all, examples, (char*)line.c_str(), (VW::example_factory_t)&VW::get_unused_example, (void*)&all); + + return examples; +} + +// TODO: Make unit test dig out and verify features. +BOOST_AUTO_TEST_CASE(parse_json_simple) +{ + auto vw = VW::initialize("--json --no_stdin", nullptr, false, nullptr, nullptr); + + std::string json_text = R"( + { + "_label": 1, + "features": { + "13": 3.9656971e-02, + "24303": 2.2660980e-01, + "const": 0.01 + } + })"; + + auto examples = parse_json(*vw, json_text); + + BOOST_CHECK_EQUAL(examples.size(), 1); + BOOST_CHECK_CLOSE(examples[0]->l.simple.label, 1.f, FLOAT_TOL); +} + +// TODO: Make unit test dig out and verify features. +BOOST_AUTO_TEST_CASE(parse_json_cb) { + std::string json_text = R"( + { + "s_": "1", + "s_": "2", + "_labelIndex": 0, + "_label_Action": 1, + "_label_Cost": 1, + "_label_Probability": 0.5, + "_multi": [ + { + "a_": "1", + "b_": "1", + "c_": "1" + }, + { + "a_": "2", + "b_": "2", + "c_": "2" + }, + { + "a_": "3", + "b_": "3", + "c_": "3" + } + ] + })"; + + auto vw = VW::initialize("--cb_adf --json --no_stdin", nullptr, false, nullptr, nullptr); + auto examples = parse_json(*vw, json_text); + BOOST_CHECK_EQUAL(examples.size(), 4); + + BOOST_CHECK_EQUAL(examples[0]->l.cb.costs.size(), 1); + BOOST_CHECK_CLOSE(examples[0]->l.cb.costs[0].probability, -1.f, FLOAT_TOL); + BOOST_CHECK_CLOSE(examples[0]->l.cb.costs[0].cost, FLT_MAX, FLOAT_TOL); + + // Action examples + BOOST_CHECK_EQUAL(examples[1]->l.cb.costs.size(), 1); + BOOST_CHECK_EQUAL(examples[2]->l.cb.costs.size(), 0); + BOOST_CHECK_EQUAL(examples[3]->l.cb.costs.size(), 0); + + BOOST_CHECK_CLOSE(examples[1]->l.cb.costs[0].probability, 0.5, FLOAT_TOL); + BOOST_CHECK_CLOSE(examples[1]->l.cb.costs[0].cost, 1.0, FLOAT_TOL); + BOOST_CHECK_EQUAL(examples[1]->l.cb.costs[0].action, 1); +} + +// TODO: Make unit test dig out and verify features. +BOOST_AUTO_TEST_CASE(parse_json_ccb) { + std::string json_text = R"( + { + "s_": "1", + "_multi": [ + { + "b_": "1", + "c_": "1", + "d_": "1" + }, + { + "b_": "2", + "c_": "2", + "d_": "2" + }, + { + "b_": "2", + "c_": "2", + "d_": "2" + }, + { + "b_": "2", + "c_": "2", + "d_": "2" + } + ], + "_df": [ + { + "_id": "00eef1eb-2205-4f47", + "_inc": [1,2], + "test": 4, + "_label_cost": 2, + "_o": [], + "_a": 1, + "_p": 0.25 + }, + { + "other_feature": 3 + }, + { + "_id": "set_id", + "other": 6, + "_label_cost": 4, + "_o": [], + "_a": [2,1], + "_p": [0.75,0.25] + } + ] + })"; + + auto vw = VW::initialize("--ccb_explore_adf --json --no_stdin", nullptr, false, nullptr, nullptr); + + auto examples = parse_json(*vw, json_text); + + BOOST_CHECK_EQUAL(examples.size(), 8); + BOOST_CHECK_EQUAL(examples[0]->l.conditional_contextual_bandit.type, CCB::example_type::shared); + BOOST_CHECK_EQUAL(examples[1]->l.conditional_contextual_bandit.type, CCB::example_type::action); + BOOST_CHECK_EQUAL(examples[2]->l.conditional_contextual_bandit.type, CCB::example_type::action); + BOOST_CHECK_EQUAL(examples[3]->l.conditional_contextual_bandit.type, CCB::example_type::action); + BOOST_CHECK_EQUAL(examples[4]->l.conditional_contextual_bandit.type, CCB::example_type::action); + BOOST_CHECK_EQUAL(examples[5]->l.conditional_contextual_bandit.type, CCB::example_type::decision); + BOOST_CHECK_EQUAL(examples[6]->l.conditional_contextual_bandit.type, CCB::example_type::decision); + BOOST_CHECK_EQUAL(examples[7]->l.conditional_contextual_bandit.type, CCB::example_type::decision); + + auto label1 = examples[5]->l.conditional_contextual_bandit; + BOOST_CHECK_EQUAL(label1.explicit_included_actions.size(), 2); + BOOST_CHECK_EQUAL(label1.explicit_included_actions[0], 1); + BOOST_CHECK_EQUAL(label1.explicit_included_actions[1], 2); + BOOST_CHECK_CLOSE(label1.outcome->cost, 2.f, .0001f); + BOOST_CHECK_EQUAL(label1.outcome->probabilities.size(), 1); + BOOST_CHECK_EQUAL(label1.outcome->probabilities[0].action, 1); + BOOST_CHECK_CLOSE(label1.outcome->probabilities[0].score, .25f, .0001f); + + auto label2 = examples[6]->l.conditional_contextual_bandit; + BOOST_CHECK_EQUAL(label2.explicit_included_actions.size(), 0); + BOOST_CHECK(label2.outcome == nullptr); + + auto label3 = examples[7]->l.conditional_contextual_bandit; + BOOST_CHECK_EQUAL(label3.explicit_included_actions.size(), 0); + BOOST_CHECK_CLOSE(label3.outcome->cost, 4.f, .0001f); + BOOST_CHECK_EQUAL(label3.outcome->probabilities.size(), 2); + BOOST_CHECK_EQUAL(label3.outcome->probabilities[0].action, 2); + BOOST_CHECK_CLOSE(label3.outcome->probabilities[0].score, .75f, .0001f); + BOOST_CHECK_EQUAL(label3.outcome->probabilities[1].action, 1); + BOOST_CHECK_CLOSE(label3.outcome->probabilities[1].score, .25f, .0001f); +} + + diff --git a/test/unit_test/test_common.h b/test/unit_test/test_common.h index 02c81cc5860..2e103789dfc 100644 --- a/test/unit_test/test_common.h +++ b/test/unit_test/test_common.h @@ -5,6 +5,8 @@ #include +constexpr float FLOAT_TOL = 0.0001f; + void inline check_float_vectors(const std::vector& lhs, const std::vector& rhs, const float tolerance_percent) { BOOST_CHECK_EQUAL(lhs.size(), rhs.size()); for (auto l = begin(lhs), r = begin(rhs); l < end(lhs); ++l, ++r) { diff --git a/test/unit_test/unit_test.vcxproj b/test/unit_test/unit_test.vcxproj index bc824f6d613..98641849218 100644 --- a/test/unit_test/unit_test.vcxproj +++ b/test/unit_test/unit_test.vcxproj @@ -1,4 +1,4 @@ - + @@ -20,7 +20,10 @@ + + + @@ -135,7 +138,7 @@ - $(SolutionDir);$(SolutionDir)\..\explore;%(AdditionalIncludeDirectories) + $(SolutionDir);$(SolutionDir)\..\explore;$(SolutionDir)\..\rapidjson\include;%(AdditionalIncludeDirectories) ZLIB_WINAPI;_WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions) /D "_CRT_SECURE_NO_WARNINGS" %(AdditionalOptions) @@ -170,4 +173,4 @@ xcopy /Y ..\..\vowpalwabbit\packages\boost_unit_test_framework-vc140.1.63.0.0\lib\native\address-model-32\lib\*.dll $(OutDir) - \ No newline at end of file + diff --git a/test/unit_test/unit_test.vcxproj.filters b/test/unit_test/unit_test.vcxproj.filters index 5b0f428cf2d..1272a543d83 100644 --- a/test/unit_test/unit_test.vcxproj.filters +++ b/test/unit_test/unit_test.vcxproj.filters @@ -33,6 +33,15 @@ Source Files + + Source Files + + + Source Files + + + Source Files + Source Files @@ -45,4 +54,4 @@ Header Files - \ No newline at end of file + diff --git a/vowpalwabbit/CMakeLists.txt b/vowpalwabbit/CMakeLists.txt index 2945cbb28cd..ebce01114f5 100644 --- a/vowpalwabbit/CMakeLists.txt +++ b/vowpalwabbit/CMakeLists.txt @@ -34,7 +34,7 @@ set(vw_all_headers gen_cs_example.h parse_args.h topk.h cb_explore_adf.h parse_dispatch_loop.h unique_sort.h interact.h interactions.h parse_example_json.h cbify.h interactions_predict.h vw_allreduce.h classweight.h parse_regressor.h kernel_svm.h confidence.h label_dictionary.h - config.h.in primitives.h lda_core.h print.h vw_versions.h + config.h.in primitives.h lda_core.h print.h vw_versions.h conditional_contextual_bandit.h ) set(vw_all_sources @@ -50,7 +50,7 @@ set(vw_all_sources active_cover.cc cs_active.cc kernel_svm.cc best_constant.cc ftrl.cc svrg.cc lrqfa.cc interact.cc comp_io.cc interactions.cc vw_validate.cc audit_regressor.cc gen_cs_example.cc cb_explore.cc action_score.cc cb_explore_adf.cc OjaNewton.cc baseline.cc classweight.cc - vw_exception.cc no_label.cc + vw_exception.cc no_label.cc conditional_contextual_bandit.cc ) set(explore_all_headers diff --git a/vowpalwabbit/cache.cc b/vowpalwabbit/cache.cc index d30d92e22f9..288da0744b2 100644 --- a/vowpalwabbit/cache.cc +++ b/vowpalwabbit/cache.cc @@ -209,3 +209,12 @@ void cache_features(io_buf& cache, example* ae, uint64_t mask) for (namespace_index ns : ae->indices) output_features(cache, ns, ae->feature_space[ns], mask); } + +uint32_t VW::convert(size_t number) +{ + if (number > UINT32_MAX) + { + THROW("size_t value is out of bounds of uint32_t.") + } + return static_cast(number); +} diff --git a/vowpalwabbit/cache.h b/vowpalwabbit/cache.h index 0f93b9fbc23..4eff4ca0e08 100644 --- a/vowpalwabbit/cache.h +++ b/vowpalwabbit/cache.h @@ -16,3 +16,18 @@ void cache_tag(io_buf& cache, v_array tag); void cache_features(io_buf& cache, example* ae, uint64_t mask); void output_byte(io_buf& cache, unsigned char s); void output_features(io_buf& cache, unsigned char index, features& fs, uint64_t mask); + +namespace VW +{ + template + T read_object(io_buf& cache) + { + char* c; + size_t next_read_size = sizeof(T); + if (cache.buf_read(c, next_read_size) < next_read_size) + THROW("Failed to read CCB label cache"); + return *reinterpret_cast(c); + } + + uint32_t convert(size_t number); +} diff --git a/vowpalwabbit/cb.cc b/vowpalwabbit/cb.cc index 1d9bcfdd4c1..3448429d04e 100644 --- a/vowpalwabbit/cb.cc +++ b/vowpalwabbit/cb.cc @@ -101,15 +101,6 @@ void copy_label(void* dst, void* src) copy_array(ldD->costs, ldS->costs); } -bool substring_eq(substring ss, const char* str) -{ - size_t len_ss = ss.end - ss.begin; - size_t len_str = strlen(str); - if (len_ss != len_str) - return false; - return (strncmp(ss.begin, str, len_ss) == 0); -} - void parse_label(parser* p, shared_data*, void* v, v_array& words) { CB::label* ld = (CB::label*)v; @@ -148,7 +139,7 @@ void parse_label(parser* p, shared_data*, void* v, v_array& words) cerr << "invalid probability < 0 specified for an action, resetting to 0." << endl; f.probability = .0; } - if (substring_eq(p->parse_name[0], "shared")) + if (substring_equal(p->parse_name[0], "shared")) { if (p->parse_name.size() == 1) { diff --git a/vowpalwabbit/conditional_contextual_bandit.cc b/vowpalwabbit/conditional_contextual_bandit.cc new file mode 100644 index 00000000000..c99fe6031ea --- /dev/null +++ b/vowpalwabbit/conditional_contextual_bandit.cc @@ -0,0 +1,532 @@ +#include "conditional_contextual_bandit.h" +#include "reductions.h" +#include "example.h" +#include "global_data.h" +#include "cache.h" +#include "vw.h" + +#include +#include +#include +#include + +using namespace LEARNER; +using namespace VW; +using namespace VW::config; + +struct ccb +{ + example* shared; + std::vector actions, decisions; + uint32_t chosen_action_index; + std::unordered_map origin_index; + CB::cb_class cb_label, default_cb_label; + std::unordered_set exclude_list, include_list; + CCB::decision_scores_t decision_scores; +}; + +namespace CCB +{ +void clear_all(ccb& data) +{ + data.shared = nullptr; + data.actions.clear(); + data.decisions.clear(); + data.chosen_action_index = 0; + data.origin_index.clear(); + data.exclude_list.clear(); + data.include_list.clear(); + data.decision_scores.clear(); +} + +// split the decisions, the actions and the shared example from the multiline example +void split_multi_example(const multi_ex& examples, ccb& data) +{ + for (auto ex : examples) + { + switch (ex->l.conditional_contextual_bandit.type) + { + case CCB::example_type::shared: + data.shared = ex; + break; + case CCB::example_type::action: + data.actions.push_back(ex); + break; + case CCB::example_type::decision: + data.decisions.push_back(ex); + break; + default: + THROW("ccb_adf_explore: badly formatted example - invalid example type"); + } + } +} + +template +void sanity_checks(ccb& data) +{ + if (data.decisions.size() > data.actions.size()) + THROW("ccb_adf_explore: badly formatted example - number of actions " + << data.actions.size() << " must be greater than the number of decisions " << data.decisions.size()); + + if (is_learn) + { + for (auto decision : data.decisions) + { + if (decision->l.conditional_contextual_bandit.outcome != nullptr && + decision->l.conditional_contextual_bandit.outcome->probabilities.size() == 0) + THROW("ccb_adf_explore: badly formatted example - missing label probability"); + } + } +} + +// create empty/default cb labels +void create_cb_labels(ccb& data) +{ + data.shared->l.cb.costs = v_init(); + data.shared->l.cb.costs.push_back(data.default_cb_label); + for (example* action : data.actions) action->l.cb.costs = v_init(); +} + +// the polylabel (union) must be manually cleaned up +void delete_cb_labels(ccb& data) +{ + data.shared->l.cb.costs.delete_v(); + for (example* action : data.actions) action->l.cb.costs.delete_v(); +} + +void attach_label_to_first_action(conditional_contexual_bandit_outcome* outcome, ccb& data) +{ + // save the cb label + data.cb_label.action = data.chosen_action_index; + data.cb_label.probability = outcome->probabilities[0].score; + data.cb_label.cost = outcome->cost; + + data.actions[0]->l.cb.costs.push_back(data.cb_label); +} + +template +void save_action_scores(ccb& data) +{ + // save a copy + auto copy = v_init(); + copy_array(copy, data.shared->pred.a_s); + data.decision_scores.push_back(copy); + + // correct indices: we want index relative to the original ccb multi-example, with no actions filtered + for (auto& action_score : copy) action_score.action = data.origin_index[action_score.action]; + + // exclude the chosen action from next decisions + if (!is_learn) + data.exclude_list.insert(copy[0].action); + else + data.exclude_list.insert(data.chosen_action_index); +} + +void clear_pred_and_label(ccb& data) +{ + data.shared->pred.a_s.clear(); + data.actions[0]->l.cb.costs.clear(); +} + +// true if there exists at least 1 action in the cb multi-example +bool has_action(multi_ex& cb_ex) { return cb_ex.size() > 1; } + +// build a cb example from the ccb example +template +void build_cb_example(multi_ex& cb_ex, example* decision, ccb& data) +{ + bool decision_has_label = decision->l.conditional_contextual_bandit.outcome != nullptr; + + // set the shared example in the cb multi-example + //TODO merge decision features in shared feature + activate interactions + cb_ex.push_back(data.shared); + + // retrieve the action index whitelist (if the list is empty, then all actions are white-listed) + data.include_list.clear(); + for (uint32_t included_action_id : decision->l.conditional_contextual_bandit.explicit_included_actions) + data.include_list.insert(included_action_id); + + // set the available actions in the cb multi-example + uint32_t index = 0; + data.origin_index.clear(); + for (size_t i = 0; i < data.actions.size(); i++) + { + // filter actions that are not explicitely included + if (!data.include_list.empty() && data.include_list.find((uint32_t)i) == data.include_list.end()) + continue; + + // filter actions chosen by previous decisions + if (data.exclude_list.find((uint32_t)i) != data.exclude_list.end()) + continue; + + // select the action + cb_ex.push_back(data.actions[i]); + + // save the original index from the root multi-example + data.origin_index[index++] = (uint32_t)i; + + // remember the index of the chosen action + if (is_learn && decision_has_label && + i == decision->l.conditional_contextual_bandit.outcome->probabilities[0].action) + data.chosen_action_index = (uint32_t)i; + } + + if (is_learn && decision_has_label && has_action(cb_ex)) + attach_label_to_first_action(decision->l.conditional_contextual_bandit.outcome, data); +} + +// iterate over decisions contained in the multi-example, and for each decision, build a cb example and perform a +// cb_explore_adf call. +template +void learn_or_predict(ccb& data, multi_learner& base, multi_ex& examples) +{ + clear_all(data); + split_multi_example(examples, data); // split shared, actions and decisions + sanity_checks(data); + create_cb_labels(data); + + // for each decision, re-build the cb example and call cb_explore_adf + for (example* decision : data.decisions) + { + multi_ex cb_ex; + build_cb_example(cb_ex, decision, data); + + if (has_action(cb_ex)) + { // the cb example contains at least 1 action + multiline_learn_or_predict(base, cb_ex, examples[0]->ft_offset); + save_action_scores(data); + clear_pred_and_label(data); + } + else + { // the cb example contains no action => cannot decide + auto empty_action_scores = v_init(); + data.decision_scores.push_back(empty_action_scores); + } + } + + delete_cb_labels(data); + + // save the predictions + // TODO fix console print: this rewrite the polylabel and thus break the print stdout happening in cb.cc + examples[0]->pred.decision_scores = data.decision_scores; +} + +void finish(ccb& data) +{ + data.actions.~vector(); + data.decisions.~vector(); + data.origin_index.~unordered_map(); + data.exclude_list.~unordered_set(); + data.include_list.~unordered_set(); + data.cb_label.~cb_class(); + data.default_cb_label.~cb_class(); +} + +base_learner* ccb_explore_adf_setup(options_i& options, vw& all) +{ + auto data = scoped_calloc_or_throw(); + bool ccb_explore_adf_option = false; + option_group_definition new_options("Conditional Contextual Bandit Exploration with Action Dependent Features"); + new_options.add(make_option("ccb_explore_adf", ccb_explore_adf_option) + .keep() + .help("Do Conditional Contextual Bandit learning with multiline action dependent features.")); + options.add_and_parse(new_options); + + if (!ccb_explore_adf_option) + return nullptr; + + if (!options.was_supplied("cb_explore_adf")) + { + options.insert("cb_explore_adf", ""); + options.add_and_parse(new_options); + } + + auto base = as_multiline(setup_base(options, all)); + all.p->lp = CCB::ccb_label_parser; + all.label_type = label_type::ccb; + + // Extract from lower level reductions + data->decision_scores = v_init(); + data->default_cb_label = {FLT_MAX, 0, -1.f, 0.f}; + data->shared = nullptr; + + learner& l = + init_learner(data, base, learn_or_predict, learn_or_predict, 1, prediction_type::decision_probs); + + l.set_finish(CCB::finish); + return make_base(l); +} + +size_t read_cached_label(shared_data*, void* v, io_buf& cache) +{ + CCB::label* ld = static_cast(v); + size_t read_count = 0; + + ld->type = static_cast(read_object(cache)); + read_count += sizeof(uint8_t); + + bool is_outcome_present = read_object(cache); + read_count += sizeof(bool); + + if (is_outcome_present) + { + ld->outcome = new CCB::conditional_contexual_bandit_outcome(); + ld->outcome->probabilities = v_init(); + + ld->outcome->cost = read_object(cache); + read_count += sizeof(float); + auto size_probs = read_object(cache); + read_count += sizeof(uint32_t); + + for (uint32_t i = 0; i < size_probs; i++) + { + ld->outcome->probabilities.push_back(read_object(cache)); + read_count += sizeof(ACTION_SCORE::action_score); + } + } + + auto size_includes = read_object(cache); + read_count += sizeof(uint32_t); + + for (uint32_t i = 0; i < size_includes; i++) + { + ld->explicit_included_actions.push_back(read_object(cache)); + read_count += sizeof(uint32_t); + } + + return read_count; +} + +float ccb_weight(void*) { return 1.; } + +void cache_label(void* v, io_buf& cache) +{ + char* c; + CCB::label* ld = static_cast(v); + size_t size = sizeof(uint8_t) // type + + sizeof(bool) // outcome exists? + + (ld->outcome == nullptr ? 0 + : sizeof(ld->outcome->cost) // cost + + sizeof(uint32_t) // probabilities size + + sizeof(ACTION_SCORE::action_score) * ld->outcome->probabilities.size()) // probabilities + + sizeof(uint32_t) // explicit_included_actions size + + sizeof(uint32_t) * ld->explicit_included_actions.size(); + + cache.buf_write(c, size); + + *(uint8_t*)c = static_cast(ld->type); + c += sizeof(uint8_t); + + *(bool*)c = ld->outcome != nullptr; + c += sizeof(bool); + + if (ld->outcome != nullptr) + { + *(float*)c = ld->outcome->cost; + c += sizeof(float); + + *(uint32_t*)c = convert(ld->outcome->probabilities.size()); + c += sizeof(uint32_t); + + for (const auto& score : ld->outcome->probabilities) + { + *(ACTION_SCORE::action_score*)c = score; + c += sizeof(ACTION_SCORE::action_score); + } + } + + *(uint32_t*)c = convert(ld->explicit_included_actions.size()); + c += sizeof(uint32_t); + + for (const auto& included_action : ld->explicit_included_actions) + { + *(uint32_t*)c = included_action; + c += sizeof(uint32_t); + } +} + +void default_label(void* v) +{ + CCB::label* ld = static_cast(v); + ld->outcome = nullptr; + ld->explicit_included_actions = v_init(); + ld->type = example_type::unset; +} + +bool test_label(void* v) +{ + CCB::label* ld = static_cast(v); + return ld->outcome == nullptr; +} + +void delete_label(void* v) +{ + CCB::label* ld = static_cast(v); + if (ld->outcome) + { + ld->outcome->probabilities.delete_v(); + delete ld->outcome; + ld->outcome = nullptr; + } + ld->explicit_included_actions.delete_v(); +} + +void copy_label(void* dst, void* src) +{ + CCB::label* ldDst = static_cast(dst); + CCB::label* ldSrc = static_cast(src); + + if (ldSrc->outcome) + { + ldDst->outcome = new CCB::conditional_contexual_bandit_outcome(); + ldDst->outcome->probabilities = v_init(); + + ldDst->outcome->cost = ldSrc->outcome->cost; + copy_array(ldDst->outcome->probabilities, ldSrc->outcome->probabilities); + } + + copy_array(ldDst->explicit_included_actions, ldSrc->explicit_included_actions); + ldDst->type = ldSrc->type; +} + +ACTION_SCORE::action_score convert_to_score(const substring& action_id_str, const substring& probability_str) +{ + auto action_id = static_cast(int_of_substring(action_id_str)); + auto probability = float_of_substring(probability_str); + if (nanpattern(probability)) + THROW("error NaN probability: " << probability_str); + + if (probability > 1.0) + { + std::cerr << "invalid probability > 1 specified for an outcome, resetting to 1.\n"; + probability = 1.0; + } + if (probability < 0.0) + { + std::cerr << "invalid probability < 0 specified for an outcome, resetting to 0.\n"; + probability = .0; + } + + return {action_id, probability}; +} + +//::,:,:,… +CCB::conditional_contexual_bandit_outcome* parse_outcome(substring& outcome) +{ + auto& ccb_outcome = *(new CCB::conditional_contexual_bandit_outcome()); + + auto split_commas = v_init(); + tokenize(',', outcome, split_commas); + + auto split_colons = v_init(); + tokenize(':', split_commas[0], split_colons); + + if (split_colons.size() != 3) + THROW("Malformed ccb label"); + + ccb_outcome.probabilities = v_init(); + ccb_outcome.probabilities.push_back(convert_to_score(split_colons[0], split_colons[2])); + + ccb_outcome.cost = float_of_substring(split_colons[1]); + if (nanpattern(ccb_outcome.cost)) + THROW("error NaN cost: " << split_colons[1]); + + split_colons.clear(); + + for (int i = 1; i < split_commas.size(); i++) + { + tokenize(':', split_commas[i], split_colons); + if (split_colons.size() != 2) + THROW("Must be action probability pairs"); + ccb_outcome.probabilities.push_back(convert_to_score(split_colons[0], split_colons[1])); + } + + split_colons.delete_v(); + split_commas.delete_v(); + + return &ccb_outcome; +} + +void parse_explicit_inclusions(CCB::label* ld, v_array& split_inclusions) +{ + for (const auto& inclusion : split_inclusions) + { + ld->explicit_included_actions.push_back(int_of_substring(inclusion)); + } +} + +void parse_label(parser* p, shared_data*, void* v, v_array& words) +{ + CCB::label* ld = static_cast(v); + + if (words.size() < 2) + THROW("ccb labels may not be empty"); + if (!substring_equal(words[0], "ccb")) + { + THROW("ccb labels require the first word to be ccb"); + } + + auto type = words[1]; + if (substring_equal(type, "shared")) + { + if (words.size() > 2) + THROW("shared labels may not have a cost"); + ld->type = CCB::example_type::shared; + } + else if (substring_equal(type, "action")) + { + if (words.size() > 2) + THROW("action labels may not have a cost"); + ld->type = CCB::example_type::action; + } + else if (substring_equal(type, "decision")) + { + if (words.size() > 4) + THROW("ccb decision label can only have a type cost and exclude list"); + ld->type = CCB::example_type::decision; + + // Skip the first two words "ccb " + for (auto i = 2; i < words.size(); i++) + { + auto is_outcome = std::find(words[i].begin, words[i].end, ':'); + if (is_outcome != words[i].end) + { + if (ld->outcome != nullptr) + { + THROW("There may be only 1 outcome associated with a decision.") + } + + ld->outcome = parse_outcome(words[i]); + } + else + { + tokenize(',', words[i], p->parse_name); + parse_explicit_inclusions(ld, p->parse_name); + } + } + + // If a full distribution has been given, check if it sums to 1, otherwise throw. + if (ld->outcome && ld->outcome->probabilities.size() > 1) + { + float total_pred = std::accumulate(ld->outcome->probabilities.begin(), ld->outcome->probabilities.end(), 0.f, + [](float result_so_far, ACTION_SCORE::action_score action_pred) { + return result_so_far + action_pred.score; + }); + + // TODO do a proper comparison here. + if (total_pred > 1.1f || total_pred < 0.9f) + { + THROW("When providing all predicition probabilties they must add up to 1.f"); + } + } + } + else + { + THROW("unknown label type: " << type); + } +} + +// Export the definition of this label parser. +label_parser ccb_label_parser = {default_label, parse_label, cache_label, read_cached_label, delete_label, ccb_weight, + copy_label, test_label, sizeof(CCB::label)}; +} // namespace CCB diff --git a/vowpalwabbit/conditional_contextual_bandit.h b/vowpalwabbit/conditional_contextual_bandit.h new file mode 100644 index 00000000000..86aace4ace6 --- /dev/null +++ b/vowpalwabbit/conditional_contextual_bandit.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include "label_parser.h" +#include "v_array.h" +#include "action_score.h" +#include "options.h" + + +namespace LEARNER { + template struct learner; + using base_learner = learner; +} + +struct vw; +struct example; +typedef std::vector multi_ex; + +namespace CCB { + // Each positon in outer array is implicitly the decision corresponding to that index. Each inner array is the result of CB for that call. + typedef v_array decision_scores_t; + + struct conditional_contexual_bandit_outcome + { + // The cost of this class + float cost; + + // Either probability for top action or for all actions in action set. + // Top action is always in first position. + ACTION_SCORE::action_scores probabilities; + }; + + enum example_type : uint8_t + { + unset = 0, + shared = 1, + action = 2, + decision = 3 + }; + + struct label { + example_type type; + // Outcome may be unset. + conditional_contexual_bandit_outcome* outcome; + v_array explicit_included_actions; + }; + + LEARNER::base_learner* ccb_explore_adf_setup(VW::config::options_i& options, vw& all); + + extern label_parser ccb_label_parser; +} diff --git a/vowpalwabbit/cost_sensitive.cc b/vowpalwabbit/cost_sensitive.cc index 2d239e6d612..8540317c48b 100644 --- a/vowpalwabbit/cost_sensitive.cc +++ b/vowpalwabbit/cost_sensitive.cc @@ -118,16 +118,7 @@ void copy_label(void* dst, void* src) } } -bool substring_eq(substring ss, const char* str) -{ - size_t len_ss = ss.end - ss.begin; - size_t len_str = strlen(str); - if (len_ss != len_str) - return false; - return (strncmp(ss.begin, str, len_ss) == 0); -} - -void parse_label(parser* p, shared_data* sd, void* v, v_array& words) +void parse_label(parser* p, shared_data*sd, void* v, v_array& words) { label* ld = (label*)v; ld->costs.clear(); @@ -137,12 +128,12 @@ void parse_label(parser* p, shared_data* sd, void* v, v_array& words) { float fx; name_value(words[0], p->parse_name, fx); - bool eq_shared = substring_eq(p->parse_name[0], "***shared***"); - bool eq_label = substring_eq(p->parse_name[0], "***label***"); + bool eq_shared = substring_equal(p->parse_name[0], "***shared***"); + bool eq_label = substring_equal(p->parse_name[0], "***label***"); if (!sd->ldict) { - eq_shared |= substring_eq(p->parse_name[0], "shared"); - eq_label |= substring_eq(p->parse_name[0], "label"); + eq_shared |= substring_equal(p->parse_name[0], "shared"); + eq_label |= substring_equal(p->parse_name[0], "label"); } if (eq_shared || eq_label) { diff --git a/vowpalwabbit/example.h b/vowpalwabbit/example.h index 9c3a167f0e3..d87606ad80b 100644 --- a/vowpalwabbit/example.h +++ b/vowpalwabbit/example.h @@ -18,6 +18,7 @@ license as described in the file LICENSE. #include "feature_group.h" #include "action_score.h" #include "example_predict.h" +#include "conditional_contextual_bandit.h" #include const unsigned char wap_ldf_namespace = 126; @@ -40,6 +41,7 @@ typedef union { MULTICLASS::label_t multi; COST_SENSITIVE::label cs; CB::label cb; + CCB::label conditional_contextual_bandit; CB_EVAL::label cb_eval; MULTILABEL::labels multilabels; } polylabel; @@ -54,6 +56,7 @@ typedef union { float scalar; v_array scalars; // a sequence of scalar predictions ACTION_SCORE::action_scores a_s; // a sequence of classes with scores. Also used for probabilities. + CCB::decision_scores_t decision_scores; uint32_t multiclass; MULTILABEL::labels multilabels; float prob; // for --probabilities --csoaa_ldf=mc @@ -85,6 +88,9 @@ struct example : public example_predict // core example datatype. bool end_pass; // special example indicating end of pass. bool sorted; // Are the features sorted or not? bool in_use; // in use or not (for the parser) + + // Interactions can be overriden on a per example basis by setting this pointer. + std::vector* override_interactions; }; struct vw; diff --git a/vowpalwabbit/gd.h b/vowpalwabbit/gd.h index 697e61cba51..87169eafea8 100644 --- a/vowpalwabbit/gd.h +++ b/vowpalwabbit/gd.h @@ -97,9 +97,9 @@ inline void foreach_feature(vw& all, example& ec, R& dat) inline float inline_predict(vw& all, example& ec) { return all.weights.sparse ? inline_predict(all.weights.sparse_weights, all.ignore_some_linear, - all.ignore_linear, all.interactions, all.permutations, ec, ec.l.simple.initial) + all.ignore_linear, ec.override_interactions ? *ec.override_interactions : all.interactions, all.permutations, ec, ec.l.simple.initial) : inline_predict(all.weights.dense_weights, all.ignore_some_linear, - all.ignore_linear, all.interactions, all.permutations, ec, ec.l.simple.initial); + all.ignore_linear, ec.override_interactions ? *ec.override_interactions : all.interactions, all.permutations, ec, ec.l.simple.initial); } inline float sign(float w) diff --git a/vowpalwabbit/global_data.h b/vowpalwabbit/global_data.h index 05328b00f0e..e8a7ae1cdf2 100644 --- a/vowpalwabbit/global_data.h +++ b/vowpalwabbit/global_data.h @@ -448,7 +448,8 @@ enum label_type_t cb_eval, // contextual-bandit evaluation cs, // cost-sensitive multi, - mc + mc, + ccb // conditional contextual-bandit }; } diff --git a/vowpalwabbit/learner.h b/vowpalwabbit/learner.h index b6bd1b5b61c..5c75ff4c182 100644 --- a/vowpalwabbit/learner.h +++ b/vowpalwabbit/learner.h @@ -22,7 +22,8 @@ enum prediction_type_t multiclass, multilabels, prob, - multiclassprobs + multiclassprobs, + decision_probs }; const char* to_string(prediction_type_t prediction_type); diff --git a/vowpalwabbit/parse_args.cc b/vowpalwabbit/parse_args.cc index 3ca2c7555a5..604caab116e 100644 --- a/vowpalwabbit/parse_args.cc +++ b/vowpalwabbit/parse_args.cc @@ -1248,7 +1248,7 @@ void parse_reductions(options_i& options, vw& all) all.reduction_stack.push(lrqfa_setup); all.reduction_stack.push(stagewise_poly_setup); all.reduction_stack.push(scorer_setup); - // Reductions + // ReductionsS all.reduction_stack.push(bs_setup); all.reduction_stack.push(binary_setup); @@ -1271,6 +1271,7 @@ void parse_reductions(options_i& options, vw& all) all.reduction_stack.push(mwt_setup); all.reduction_stack.push(cb_explore_setup); all.reduction_stack.push(cb_explore_adf_setup); + all.reduction_stack.push(CCB::ccb_explore_adf_setup); all.reduction_stack.push(cbify_setup); all.reduction_stack.push(cbifyldf_setup); all.reduction_stack.push(explore_eval_setup); diff --git a/vowpalwabbit/parse_example_json.h b/vowpalwabbit/parse_example_json.h index ee5ed3d037c..af924df53f9 100644 --- a/vowpalwabbit/parse_example_json.h +++ b/vowpalwabbit/parse_example_json.h @@ -28,6 +28,8 @@ license as described in the file LICENSE. #endif #include "cb.h" +#include "conditional_contextual_bandit.h" + #include "best_constant.h" #include @@ -49,6 +51,16 @@ struct BaseState; template struct Context; +template +struct json_parser; + +enum json_parser_mode +{ + standard, + cb, + ccb +}; + template struct Namespace { @@ -160,6 +172,10 @@ class LabelObjectState : public BaseState bool found_cb; public: + std::vector actions; + std::vector probs; + std::vector inc; + LabelObjectState() : BaseState("LabelObject") {} void init(vw* /* all */) @@ -169,7 +185,7 @@ class LabelObjectState : public BaseState cb_label = {0., 0, 0., 0.}; } - BaseState* StartObject(Context& ctx) + BaseState* StartObject(Context& ctx) override { ctx.all->p->lp.default_label(&ctx.ex->l); @@ -186,14 +202,14 @@ class LabelObjectState : public BaseState return this; } - BaseState* Key(Context& ctx, const char* str, rapidjson::SizeType len, bool /* copy */) + BaseState* Key(Context& ctx, const char* str, rapidjson::SizeType len, bool /* copy */) override { ctx.key = str; ctx.key_length = len; return this; } - BaseState* Float(Context& ctx, float v) + BaseState* Float(Context& ctx, float v) override { // simple if (!_stricmp(ctx.key, "Label")) @@ -236,11 +252,42 @@ class LabelObjectState : public BaseState return this; } - BaseState* Uint(Context& ctx, unsigned v) { return Float(ctx, (float)v); } + BaseState* Uint(Context& ctx, unsigned v) override { return Float(ctx, (float)v); } - BaseState* EndObject(Context& ctx, rapidjson::SizeType) + BaseState* EndObject(Context& ctx, rapidjson::SizeType) override { - if (found_cb) + auto jsonp = static_cast*>(ctx.all->p->jsonp.get()); + if (jsonp->mode == json_parser_mode::ccb) + { + auto ld = (CCB::label*)&ctx.ex->l; + + for (auto id : inc) + { + ld->explicit_included_actions.push_back(id); + } + inc.clear(); + + if ((actions.size() != 0) && (probs.size() != 0) && (cb_label.cost != 0.f)) + { + auto outcome = new CCB::conditional_contexual_bandit_outcome(); + outcome->cost = cb_label.cost; + if (actions.size() != probs.size()) + { + THROW("Actions and probabilties must be the same length."); + } + + for (size_t i = 0; i < this->actions.size(); i++) + { + outcome->probabilities.push_back({actions[i], probs[i]}); + } + actions.clear(); + probs.clear(); + + ld->outcome = outcome; + cb_label = {0., 0, 0., 0.}; + } + } + else if (found_cb) { CB::label* ld = (CB::label*)&ctx.ex->l; ld->costs.push_back(cb_label); @@ -266,7 +313,7 @@ struct LabelSinglePropertyState : BaseState LabelSinglePropertyState() : BaseState("LabelSingleProperty") {} // forward _label - BaseState* Float(Context& ctx, float v) + BaseState* Float(Context& ctx, float v) override { // skip "_label_" ctx.key += 7; @@ -278,7 +325,7 @@ struct LabelSinglePropertyState : BaseState return ctx.previous_state; } - BaseState* Uint(Context& ctx, unsigned v) + BaseState* Uint(Context& ctx, unsigned v) override { // skip "_label_" ctx.key += 7; @@ -298,7 +345,7 @@ struct LabelIndexState : BaseState LabelIndexState() : BaseState("LabelIndex"), index(-1) {} - BaseState* Uint(Context& ctx, unsigned int v) + BaseState* Uint(Context& ctx, unsigned int v) override { index = v; return ctx.previous_state; @@ -312,9 +359,9 @@ struct LabelState : BaseState { LabelState() : BaseState("Label") {} - BaseState* StartObject(Context& ctx) { return ctx.label_object_state.StartObject(ctx); } + BaseState* StartObject(Context& ctx) override { return ctx.label_object_state.StartObject(ctx); } - BaseState* String(Context& ctx, const char* str, rapidjson::SizeType /* len */, bool copy) + BaseState* String(Context& ctx, const char* str, rapidjson::SizeType /* len */, bool copy) override { // only to be used with copy=false assert(!copy); @@ -323,14 +370,14 @@ struct LabelState : BaseState return ctx.previous_state; } - BaseState* Float(Context& ctx, float v) + BaseState* Float(Context& ctx, float v) override { // TODO: once we introduce label types, check here ctx.ex->l.simple.label = v; return ctx.previous_state; } - BaseState* Uint(Context& ctx, unsigned v) + BaseState* Uint(Context& ctx, unsigned v) override { // TODO: once we introduce label types, check here ctx.ex->l.simple.label = (float)v; @@ -344,7 +391,7 @@ struct TextState : BaseState { TextState() : BaseState("text") {} - BaseState* String(Context& ctx, const char* str, rapidjson::SizeType length, bool copy) + BaseState* String(Context& ctx, const char* str, rapidjson::SizeType length, bool copy) override { // only to be used with copy=false assert(!copy); @@ -388,7 +435,7 @@ struct TagState : BaseState // "_tag":"abc" TagState() : BaseState("tag") {} - BaseState* String(Context& ctx, const char* str, SizeType length, bool copy) + BaseState* String(Context& ctx, const char* str, SizeType length, bool copy) override { // only to be used with copy=false assert(!copy); @@ -404,10 +451,12 @@ struct MultiState : BaseState { MultiState() : BaseState("Multi") {} - BaseState* StartArray(Context& ctx) + BaseState* StartArray(Context& ctx) override { + auto jsonp = static_cast*>(ctx.all->p->jsonp.get()); + // mark shared example - if (ctx.all->label_type == label_type::cb) + if (jsonp->mode == json_parser_mode::cb) { CB::label* ld = &ctx.ex->l.cb; CB::cb_class f; @@ -419,17 +468,28 @@ struct MultiState : BaseState ld->costs.push_back(f); } + else if (jsonp->mode == json_parser_mode::ccb) + { + CCB::label* ld = &ctx.ex->l.conditional_contextual_bandit; + ld->type = CCB::example_type::shared; + } else - THROW("label type is not CB") + THROW("label type is not CB or CCB") return this; } - BaseState* StartObject(Context& ctx) + BaseState* StartObject(Context& ctx) override { // allocate new example ctx.ex = &(*ctx.example_factory)(ctx.example_factory_context); ctx.all->p->lp.default_label(&ctx.ex->l); + auto jsonp = static_cast*>(ctx.all->p->jsonp.get()); + if (jsonp->mode == json_parser_mode::ccb) + { + ctx.ex->l.conditional_contextual_bandit.type = CCB::example_type::action; + } + ctx.examples->push_back(ctx.ex); // setup default namespace @@ -438,7 +498,7 @@ struct MultiState : BaseState return &ctx.default_state; } - BaseState* EndArray(Context& ctx, rapidjson::SizeType) + BaseState* EndArray(Context& ctx, rapidjson::SizeType) override { // return to shared example ctx.ex = (*ctx.examples)[0]; @@ -447,6 +507,54 @@ struct MultiState : BaseState } }; +// This state makes the assumption we are in CCB +template +struct DfState : BaseState +{ + DfState() : BaseState("Df") {} + BaseState* saved; + BaseState* saved_root_state; + + BaseState* StartArray(Context& ctx) override + { + // drain existing added namespace + // todo check bounds + saved = ctx.PopNamespace(); + saved_root_state = ctx.root_state; + ctx.root_state = this; + return this; + } + + BaseState* StartObject(Context& ctx) override + { + // allocate new example + ctx.ex = &(*ctx.example_factory)(ctx.example_factory_context); + ctx.all->p->lp.default_label(&ctx.ex->l); + ctx.ex->l.conditional_contextual_bandit.type = CCB::example_type::decision; + + ctx.examples->push_back(ctx.ex); + + // The end object logic assumes shared example so we need to take an extra one here. + ctx.label_index_state.index = ctx.examples->size() - 2; + + // setup default namespace + ctx.PushNamespace(" ", this); + + return &ctx.default_state; + } + + BaseState* EndArray(Context& ctx, rapidjson::SizeType) override + { + // return to shared example + ctx.ex = (*ctx.examples)[0]; + + ctx.PushNamespace(" ", saved); + ctx.root_state = saved_root_state; + + return &ctx.default_state; + } +}; + // "...":[Numbers only] template class ArrayState : public BaseState @@ -456,7 +564,7 @@ class ArrayState : public BaseState public: ArrayState() : BaseState("Array") {} - BaseState* StartArray(Context& ctx) + BaseState* StartArray(Context& ctx) override { if (ctx.previous_state == this) { @@ -471,7 +579,7 @@ class ArrayState : public BaseState return this; } - BaseState* Float(Context& ctx, float f) + BaseState* Float(Context& ctx, float f) override { if (audit) { @@ -487,15 +595,15 @@ class ArrayState : public BaseState return this; } - BaseState* Uint(Context& ctx, unsigned f) { return Float(ctx, (float)f); } + BaseState* Uint(Context& ctx, unsigned f) override { return Float(ctx, (float)f); } - BaseState* Null(Context& /* ctx */) + BaseState* Null(Context& /* ctx */) override { // ignore null values and stay in current state return this; } - BaseState* StartObject(Context& ctx) + BaseState* StartObject(Context& ctx) override { // parse properties ctx.PushNamespace(ctx.namespace_path.size() > 0 ? ctx.CurrentNamespace().name : " ", this); @@ -503,7 +611,10 @@ class ArrayState : public BaseState return &ctx.default_state; } - BaseState* EndArray(Context& ctx, rapidjson::SizeType /* elementCount */) { return ctx.PopNamespace(); } + BaseState* EndArray(Context& ctx, rapidjson::SizeType /* elementCount */) override + { + return ctx.PopNamespace(); + } }; // only 0 is valid as DefaultState::Ignore injected that into the source stream @@ -512,7 +623,7 @@ struct IgnoreState : BaseState { IgnoreState() : BaseState("Ignore") {} - BaseState* Uint(Context& ctx, unsigned) { return ctx.previous_state; } + BaseState* Uint(Context& ctx, unsigned) override { return ctx.previous_state; } }; template @@ -607,7 +718,7 @@ class DefaultState : public BaseState return &ctx.ignore_state; } - BaseState* Key(Context& ctx, const char* str, rapidjson::SizeType length, bool copy) + BaseState* Key(Context& ctx, const char* str, rapidjson::SizeType length, bool copy) override { // only to be used with copy=false assert(!copy); @@ -618,38 +729,62 @@ class DefaultState : public BaseState if (length > 0 && str[0] == '_') { // match _label* - if (ctx.key_length >= 6 && !strncmp(str, "_label", 6)) + if (ctx.key_length >= 6 && !strncmp(ctx.key, "_label", 6)) { if (ctx.key_length >= 7 && ctx.key[6] == '_') return &ctx.label_single_property_state; else if (ctx.key_length == 6) return &ctx.label_state; - else if (ctx.key_length == 11 && !_stricmp(str, "_labelIndex")) + else if (ctx.key_length == 11 && !_stricmp(ctx.key, "_labelIndex")) return &ctx.label_index_state; else { - ctx.error() << "Unsupported key '" << str << "' len: " << length; + ctx.error() << "Unsupported key '" << ctx.key << "' len: " << length; return nullptr; } } - if (ctx.key_length == 5 && !strcmp(str, "_text")) + if (ctx.key_length == 5 && !strcmp(ctx.key, "_text")) return &ctx.text_state; // TODO: _multi in _multi... - if (ctx.key_length == 6 && !strcmp(str, "_multi")) + if (ctx.key_length == 6 && !strcmp(ctx.key, "_multi")) return &ctx.multi_state; + if (ctx.key_length == 3 && !strcmp(ctx.key, "_df")) + return &ctx.df_state; + if (ctx.key_length == 4 && !_stricmp(ctx.key, "_tag")) return &ctx.tag_state; + if (ctx.key_length == 4 && !_stricmp(ctx.key, "_inc")) + { + ctx.array_uint_state.output_array = &ctx.label_object_state.inc; + ctx.array_uint_state.return_state = this; + return &ctx.array_uint_state; + } + + if (ctx.key_length == 2 && ctx.key[1] == 'a') + { + ctx.array_uint_state.output_array = &ctx.label_object_state.actions; + ctx.array_uint_state.return_state = this; + return &ctx.array_uint_state; + } + + if (ctx.key_length == 2 && ctx.key[1] == 'p') + { + ctx.array_float_state.output_array = &ctx.label_object_state.probs; + ctx.array_float_state.return_state = this; + return &ctx.array_float_state; + } + return Ignore(ctx, length); } return this; } - BaseState* String(Context& ctx, const char* str, rapidjson::SizeType length, bool copy) + BaseState* String(Context& ctx, const char* str, rapidjson::SizeType length, bool copy) override { assert(!copy); @@ -675,7 +810,7 @@ class DefaultState : public BaseState return this; } - BaseState* Bool(Context& ctx, bool b) + BaseState* Bool(Context& ctx, bool b) override { if (b) ctx.CurrentNamespace().AddFeature(ctx.all, ctx.key); @@ -683,13 +818,13 @@ class DefaultState : public BaseState return this; } - BaseState* StartObject(Context& ctx) + BaseState* StartObject(Context& ctx) override { ctx.PushNamespace(ctx.key, this); return this; } - BaseState* EndObject(Context& ctx, rapidjson::SizeType memberCount) + BaseState* EndObject(Context& ctx, rapidjson::SizeType memberCount) override { BaseState* return_state = ctx.PopNamespace(); @@ -723,7 +858,7 @@ class DefaultState : public BaseState return ctx.namespace_path.empty() ? ctx.root_state : return_state; } - BaseState* Float(Context& ctx, float f) + BaseState* Float(Context& ctx, float f) override { auto& ns = ctx.CurrentNamespace(); ns.AddFeature(f, VW::hash_feature(*ctx.all, ctx.key, ns.namespace_hash), ctx.key); @@ -731,9 +866,9 @@ class DefaultState : public BaseState return this; } - BaseState* Uint(Context& ctx, unsigned f) { return Float(ctx, (float)f); } + BaseState* Uint(Context& ctx, unsigned f) override { return Float(ctx, (float)f); } - BaseState* StartArray(Context& ctx) { return ctx.array_state.StartArray(ctx); } + BaseState* StartArray(Context& ctx) override { return ctx.array_state.StartArray(ctx); } }; template @@ -743,8 +878,12 @@ class ArrayToVectorState : public BaseState ArrayToVectorState() : BaseState("ArrayToVectorState") {} std::vector* output_array; + BaseState* return_state; + + // Allows for single value handling. + bool has_seen_array_start = false; - BaseState* StartArray(Context& ctx) + BaseState* StartArray(Context& ctx) override { if (ctx.previous_state == this) { @@ -752,31 +891,53 @@ class ArrayToVectorState : public BaseState return nullptr; } + has_seen_array_start = true; + return this; } - BaseState* Uint(Context& /* ctx */, unsigned f) + BaseState* Uint(Context& /* ctx */, unsigned f) override { - output_array->push_back((T)f); + output_array->push_back(static_cast(f)); + + if (!has_seen_array_start) + { + has_seen_array_start = false; + return return_state; + } + return this; } - BaseState* Float(Context& /* ctx */, float f) + BaseState* Float(Context& /* ctx */, float f) override { - output_array->push_back((T)f); + output_array->push_back(static_cast(f)); + + if (!has_seen_array_start) + { + has_seen_array_start = false; + return return_state; + } + return this; } - BaseState* Null(Context& /* ctx */) + BaseState* Null(Context& /* ctx */) override { + if (!has_seen_array_start) + { + has_seen_array_start = false; + return return_state; + } + // ignore null values and stay in current state return this; } - BaseState* EndArray(Context& ctx, rapidjson::SizeType) + BaseState* EndArray(Context& /*ctx*/, rapidjson::SizeType /*length*/) override { - // TODO: introduce return_state - return &ctx.decision_service_state; + has_seen_array_start = false; + return return_state; } }; @@ -787,21 +948,15 @@ class StringToStringState : public BaseState StringToStringState() : BaseState("StringToStringState") {} std::string* output_string; + BaseState* return_state; - BaseState* String(Context& ctx, const char* str, rapidjson::SizeType length, bool /* copy */) + BaseState* String(Context& /*ctx*/, const char* str, rapidjson::SizeType length, bool /* copy */) override { output_string->assign(str, str + length); - - // TODO: introduce return_state - return &ctx.decision_service_state; + return return_state; } - BaseState* Null(Context& ctx) - { - // ignore null values and stay in current state - // TODO: introduce return_state - return &ctx.decision_service_state; - } + BaseState* Null(Context& /*ctx*/) override { return return_state; } }; template @@ -811,21 +966,18 @@ class FloatToFloatState : public BaseState FloatToFloatState() : BaseState("FloatToFloatState") {} float* output_float; + BaseState* return_state; - BaseState* Float(Context& ctx, float f) + BaseState* Float(Context& /*ctx*/, float f) override { *output_float = f; - - // TODO: introduce return_state - return &ctx.decision_service_state; + return return_state; } - BaseState* Null(Context& ctx) + BaseState* Null(Context& /*ctx*/) override { *output_float = 0.f; - - // TODO: introduce return_state - return &ctx.decision_service_state; + return return_state; } }; @@ -836,13 +988,12 @@ class BoolToBoolState : public BaseState BoolToBoolState() : BaseState("BoolToBoolState") {} bool* output_bool; + BaseState* return_state; - BaseState* Bool(Context& ctx, bool b) + BaseState* Bool(Context& /*ctx*/, bool b) override { *output_bool = b; - - // TODO: introduce return_state - return &ctx.decision_service_state; + return return_state; } }; @@ -856,6 +1007,65 @@ struct DecisionServiceInteraction bool skipLearn{false}; }; +template +class DecisionListState : public BaseState +{ + int decision_object_index = 0; + + std::vector actions; + std::vector probs; + float cost; + + BaseState* old_root; + + public: + DecisionListState() : BaseState("DecisionList") {} + + BaseState* StartArray(Context& ctx) override + { + decision_object_index = 0; + + // Find start index of decision objects by iterating until we find the first decision example. + for (auto ex : *ctx.examples) + { + if (ex->l.conditional_contextual_bandit.type != CCB::example_type::decision) + { + decision_object_index++; + } + } + old_root = ctx.root_state; + ctx.root_state = this; + + if (decision_object_index == 0) + { + THROW("Badly formed ccb example. Shared example is required.") + } + + return this; + } + + BaseState* StartObject(Context& ctx) override + { + // Set current example so that default state correctly sets the label. + ctx.ex = (*ctx.examples)[decision_object_index]; + // The end object logic assumes shared example so we need to take one here. + ctx.label_index_state.index = decision_object_index - 1; + + decision_object_index++; + + // Push a namespace so that default state can get back here when it reaches the end of the object. + ctx.PushNamespace(" ", this); + + return &ctx.default_state; + } + + BaseState* EndArray(Context& ctx, rapidjson::SizeType) override + { + ctx.root_state = old_root; + return &ctx.decision_service_state; + } +}; + template class DecisionServiceState : public BaseState { @@ -864,19 +1074,19 @@ class DecisionServiceState : public BaseState DecisionServiceInteraction* data; - BaseState* StartObject(Context& /* ctx */) + BaseState* StartObject(Context& /* ctx */) override { // TODO: improve validation return this; } - BaseState* EndObject(Context& /* ctx */, rapidjson::SizeType /* memberCount */) + BaseState* EndObject(Context& /* ctx */, rapidjson::SizeType /* memberCount */) override { // TODO: improve validation return this; } - BaseState* Key(Context& ctx, const char* str, rapidjson::SizeType length, bool /* copy */) + BaseState* Key(Context& ctx, const char* str, rapidjson::SizeType length, bool /* copy */) override { if (length == 1) { @@ -884,9 +1094,11 @@ class DecisionServiceState : public BaseState { case 'a': ctx.array_uint_state.output_array = &data->actions; + ctx.array_uint_state.return_state = this; return &ctx.array_uint_state; case 'p': ctx.array_float_state.output_array = &data->probabilities; + ctx.array_float_state.return_state = this; return &ctx.array_float_state; case 'c': ctx.key = " "; @@ -897,11 +1109,13 @@ class DecisionServiceState : public BaseState else if (length == 5 && !strcmp(str, "pdrop")) { ctx.float_state.output_float = &data->probabilityOfDrop; + ctx.float_state.return_state = this; return &ctx.float_state; } else if (length == 7 && !strcmp(str, "EventId")) { ctx.string_state.output_string = &data->eventId; + ctx.string_state.return_state = this; return &ctx.string_state; } else if (length > 0 && str[0] == '_') @@ -921,8 +1135,13 @@ class DecisionServiceState : public BaseState else if (length == 10 && !strncmp(str, "_skipLearn", 10)) { ctx.bool_state.output_bool = &data->skipLearn; + ctx.bool_state.return_state = this; return &ctx.bool_state; } + else if (length == 10 && !strncmp(str, "_decisions", 10)) + { + return &ctx.decision_list_state; + } } // ignore unknown properties @@ -968,6 +1187,7 @@ struct Context MultiState multi_state; IgnoreState ignore_state; ArrayState array_state; + DfState df_state; // DecisionServiceState DecisionServiceState decision_service_state; @@ -976,6 +1196,7 @@ struct Context StringToStringState string_state; FloatToFloatState float_state; BoolToBoolState bool_state; + DecisionListState decision_list_state; BaseState* root_state; @@ -1108,8 +1329,13 @@ struct VWReaderHandler : public rapidjson::BaseReaderHandler, template struct json_parser { + json_parser(json_parser_mode mode) + : mode{mode} + {} + rapidjson::Reader reader; VWReaderHandler handler; + json_parser_mode mode; }; namespace VW @@ -1153,14 +1379,20 @@ void read_line_decision_service_json(vw& all, v_array& examples, char* } InsituStringStream ss(line); - json_parser parser; + json_parser* parser = static_cast*>(all.p->jsonp.get()); - VWReaderHandler& handler = parser.handler; + // As long as VW was configured correctly this should not occur. + if(parser->mode == json_parser_mode::standard) + { + THROW("dsjson does not support standard json parser mode.") + } + + VWReaderHandler& handler = parser->handler; handler.init(&all, &examples, &ss, line + length, example_factory, ex_factory_context); handler.ctx.SetStartStateToDecisionService(data); ParseResult result = - parser.reader.template Parse>(ss, handler); + parser->reader.template Parse>(ss, handler); if (!result.IsError()) return; diff --git a/vowpalwabbit/parse_primitives.cc b/vowpalwabbit/parse_primitives.cc index cee067a92d9..5a834c32e28 100644 --- a/vowpalwabbit/parse_primitives.cc +++ b/vowpalwabbit/parse_primitives.cc @@ -22,6 +22,14 @@ bool substring_equal(const substring& a, const substring& b) && (strncmp(a.begin, b.begin, a.end - a.begin) == 0); } +bool substring_equal(const substring& ss, const char* str) +{ + size_t len_ss = ss.end - ss.begin; + size_t len_str = strlen(str); + if (len_ss != len_str) return false; + return (strncmp(ss.begin, str, len_ss) == 0); +} + uint64_t hashstring(substring s, uint64_t h) { // trim leading whitespace but not UTF-8 diff --git a/vowpalwabbit/parse_primitives.h b/vowpalwabbit/parse_primitives.h index ec7e23dbd4b..74f074036a2 100644 --- a/vowpalwabbit/parse_primitives.h +++ b/vowpalwabbit/parse_primitives.h @@ -50,6 +50,7 @@ void tokenize(char delim, substring s, ContainerT& ret, bool allow_empty = false } bool substring_equal(const substring& a, const substring& b); +bool substring_equal(const substring& ss, const char* str); inline char* safe_index(char* start, char v, char* max) { diff --git a/vowpalwabbit/parser.cc b/vowpalwabbit/parser.cc index 7558dbdecad..21c9df2dea2 100644 --- a/vowpalwabbit/parser.cc +++ b/vowpalwabbit/parser.cc @@ -534,6 +534,16 @@ void enable_sources(vw& all, bool quiet, size_t passes, input_options& input_opt if (input_options.json || input_options.dsjson) { + auto mode = json_parser_mode::standard; + if (all.label_type == label_type::cb) + { + mode = json_parser_mode::cb; + } + else if (all.label_type == label_type::ccb) + { + mode = json_parser_mode::ccb; + } + // TODO: change to class with virtual method // --invert_hash requires the audit parser version to save the extra information. if (all.audit || all.hash_inv) @@ -541,14 +551,14 @@ void enable_sources(vw& all, bool quiet, size_t passes, input_options& input_opt all.p->reader = &read_features_json; all.p->text_reader = &line_to_examples_json; all.p->audit = true; - all.p->jsonp = std::make_shared>(); + all.p->jsonp = std::make_shared>(mode); } else { all.p->reader = &read_features_json; all.p->text_reader = &line_to_examples_json; all.p->audit = false; - all.p->jsonp = std::make_shared>(); + all.p->jsonp = std::make_shared>(mode); } all.p->decision_service_json = input_options.dsjson; diff --git a/vowpalwabbit/vw_core.vcxproj b/vowpalwabbit/vw_core.vcxproj index 886a3cbc1a6..b3b8b3c5f26 100644 --- a/vowpalwabbit/vw_core.vcxproj +++ b/vowpalwabbit/vw_core.vcxproj @@ -155,6 +155,7 @@ + @@ -258,6 +259,7 @@ + @@ -373,4 +375,4 @@ - \ No newline at end of file +