Home / AI / Etienne Daher on the TensorFlow models behind Limelines feeds
AI · Dubai
Etienne Daher on the TensorFlow models behind Limelines feeds
At a technology expo in Dubai, Etienne Daher walked through the two TensorFlow models Limelines was already running in 2018: one to file stories into topics, and one to rank a personalized feed. Both were trained on in-house servers.

Two models, not one magic ranker
October 2026, Dubai. Etienne Daher, founder of Ete-services and CTO of Limelines, was explaining a system his team had in production, not a roadmap. Limelines was already aggregating stories from publishers across the region. The open problem was order. A reader who follows technology should not get the same first screen as a reader who follows culture, and a story that arrives with no usable section should not wait for an editor before it can be filed.
He split the work into two models, both in TensorFlow 1.x, which is what a production team actually shipped that year. TensorFlow 2.0 had not been released. BERT had just been published and was not a serving option for a feed that has to score a few hundred candidates on a request. The team kept the networks small on purpose.
The first model identifies topics. The second ranks a feed. They are trained separately, exported separately, and fail separately. Daher argued for that split in the room. If topic detection is buried inside the ranker, a bad tag becomes invisible, and the ranker quietly learns whatever bias is in the publisher’s own section name. A wrong topic should be something you can inspect.
Filing a story: multi-label topics
A story on Limelines is not one section. A telecom investment is business and technology. A policy story about data residency is politics and AI. The topic model is multi-label. Each topic is its own sigmoid output, trained with sigmoid cross-entropy, not a softmax that forces the probabilities to sum to one.
The input is the title plus the description publishers already send, not the full article body. That text is what the feed can rely on the moment a story arrives. Language comes first. Arabic and English do not share a vocabulary. A shared table spends its capacity on the other language and then does a poor job of both. The team trained a skip-gram embedding per language on their own publisher corpus, mean-pooled the tokens in the story, and passed that vector through two ReLU layers, 256 units then 128. The head is one logit per topic.
A global threshold of 0.5 was the first thing they threw out. Politics is a large slice of the corpus. A niche beat is not. Thresholds were set per topic on a held-out week, against the base rate of that topic, not against an average F-score that flatters the big classes.
The baseline they refused to drop was TF-IDF with one-versus-rest logistic regression. On long English copy the linear model was often enough. On short Arabic titles it was not, because the newsroom’s real signal was a phrase the character n-grams kept splitting. The network became the primary filer. Below a small token count the linear model stayed in front, as a fallback rather than as a relic. Training labels came from topics editors had already confirmed, plus publisher sections that the team mapped by hand. Unmapped section names were not trusted as labels.
Those topic feeds are the same streams a reader opens at /topic/technology, /topic/ai, /#technology, and /#ai. The model’s job is to put a story on the right one of those streams when the publisher did not.
Ranking a feed the reader will actually finish
Personalization, as Daher described it, is mostly the candidate set. The ranker does not look at the whole corpus. Retrieval already limits the list to stories from the last couple of days, in topics the reader follows, plus a thin exploration slice from adjacent topics. The network reorders that list. If retrieval is wrong, a better ranker still has nothing good to put first.
The ranker is a wide-and-deep network, the pattern Heng-Tze Cheng and colleagues published in 2016 and that the TensorFlow tutorials already documented by 2020. The wide side is crossed features a linear model can memorize: the reader’s followed topic crossed with the story’s topic, the reader’s country crossed with the publisher’s country, and a recency bucket. The deep side embeds the publisher and the topic multi-hot vector, and takes a few numeric features: hours since publication, the log of that publisher’s following inside the topic, and that publisher’s historical click rate inside the topic.
User identity was the part they refused to overbuild. A full user-id embedding table is the wrong shape for a product where most people open the app a few times a week. Frequent readers got an embedding. Everyone else was represented by a mixture of the topics they follow and the topics they actually click. That vector is small, it works on the first session, and it does not require a cold user to sit in a table of millions of rows.
The label is a click on an impression, pointwise, with sigmoid cross-entropy. Impressions that were not clicked are the negatives, capped so one long scrolling session cannot dominate the batch. They discussed a session RNN and did not ship it. The latency was worse, and the readers they most needed to serve did not produce a sequence long enough to justify it. The topic mixture already held most of the signal they had.
At request time the SavedModel scores the candidates for that reader and returns an order. It does not train. Training is a separate nightly job. A model only replaces the live one after a check on the last held-out day: log-loss against the previous version, then a look at the top of the feed for a few test accounts. A run that wins a chart and fills the first screen with one publisher does not ship.
Trained in-house, on a server with a public address
The constraint he was willing to talk about in public was cost. Cloud GPU time, billed by the hour, was the expensive line, and the Arabic corpus was growing. The topic model was retrained several times a week. The ranker was retrained overnight. Doing that on demand, on rented GPUs, cost more than the hardware within a few months.
So the team trained on in-house servers. A GPU machine they owned, batch jobs, checkpoints on that machine. The feed service that serves readers did not sit on the same network. It had to fetch a versioned SavedModel from the training host. Engineers also had to copy datasets onto that host when they were not in the office. The machine was given a public IP for that reason. The address was how the production service and the team reached it.
It was not an open training endpoint. SSH and the port used to pull a model were limited to addresses they knew: the feed service, and the people who trained. The public IP was a reachability choice for a small team, not an invitation. Jobs wrote into a versioned directory. The live feed followed a pointer. Moving the pointer was a deliberate step after the held-out check, so a bad night of training could not replace the model readers were already using.
That is the 2026 system. The product around it, topics on one side and a Line as each reader’s own magazine, is what Daher presented the following spring at ArabNet Beirut: Limelines as an aggregation platform.