Why do some rows returned by sys.dm_exec_query_profiles have "???" for the physical operator name?
Batch mode adapters (places in a query plan in which row processing switches to batch processing or the other way around) show up as ??? in the DMV with a thread_id of 0. However, the example query doesn't use batch processing so that isn't the cause here.
Nested loops prefetching can also be responsible for extra rows showing up in sys.dm_exec_query_profiles. There is a documented trace flag for disabling nested loop prefetching:

Trace flag 8744 disables pre-fetching for the Nested Loops operator.
Incorrect use of this trace flag may cause additional physical reads when SQL Server executes plans that contain the Nested Loops operator. For more information about the Nested Loops operator, see the &quot;Logical and physical operators reference&quot; topic in SQL Server 2005 Books Online.

If I add a query hint of QUERYTRACEON 8744 to the query then the ??? nodes no longer appear.
For a reproducible example of nested loop prefetching I'm going to borrow Paul White's example against Adventure Works from his Nested Loops Prefetching article:
SELECT TOP (1000)
    P.Name,
    TH.TransactionID
FROM Production.Product AS P
JOIN Production.TransactionHistory AS TH
    ON TH.ProductID = P.ProductID
WHERE
    P.Name LIKE N'[K-P]%'
ORDER BY 
    P.Name, 
    TH.TransactionID;

If I run that query against SQL Server 2016 SP1 and quickly capture the output of sys.dm_exec_query_profiles I get the following results:
╔════════════════════╦════════════════════════╦═════════╦═══════════╗
║    OBJECT_NAME     ║ physical_operator_name ║ node_id ║ thread_id ║
╠════════════════════╬════════════════════════╬═════════╬═══════════╣
║ NULL               ║ Top                    ║       0 ║         0 ║
║ NULL               ║ Nested Loops           ║       1 ║         0 ║
║ TransactionHistory ║ ???                    ║       2 ║         0 ║
║ Product            ║ Index Seek             ║       3 ║         0 ║
║ TransactionHistory ║ Index Seek             ║       4 ║         0 ║
╚════════════════════╩════════════════════════╩═════════╩═══════════╝

If I run the same query in SQL Server 2014 I get these results:
╔════════════════════╦════════════════════════╦═════════╦═══════════╗
║    OBJECT_NAME     ║ physical_operator_name ║ node_id ║ thread_id ║
╠════════════════════╬════════════════════════╬═════════╬═══════════╣
║ NULL               ║ Top                    ║       0 ║         0 ║
║ NULL               ║ Nested Loops           ║       1 ║         0 ║
║ Product            ║ Index Seek             ║       3 ║         0 ║
║ TransactionHistory ║ Index Seek             ║       4 ║         0 ║
╚════════════════════╩════════════════════════╩═════════╩═══════════╝

In both cases the nested loop prefetch optimization happens. It appears that only SQL Server 2016 reports it though which could explain why I've never seen this in SQL Server 2014.